-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathbuild.py
More file actions
320 lines (256 loc) · 9.27 KB
/
build.py
File metadata and controls
320 lines (256 loc) · 9.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
#!/usr/bin/env python3
"""
Build script for Spartan Write.
This script:
1. Updates git submodules from their remotes
2. Builds the CLI wheel using uv
3. Creates a PyInstaller executable with target triple naming
4. Copies the sidecar to gui/src-tauri/bin/
5. Runs tauri build to generate the final app bundle
"""
print("Starting build script...")
import platform
import shutil
import subprocess
import sys
import tarfile
import tempfile
import urllib.request
import zipfile
import os
from pathlib import Path
# Paths
ROOT = Path(__file__).parent
SIDECAR_DIR = ROOT / "sidecar"
FRONTEND_DIR = ROOT / "frontend"
SIDECAR_BIN_DIR = FRONTEND_DIR / "src-tauri" / "bin"
# Tectonic binary download
TECTONIC_VERSION = "0.16.8"
TECTONIC_RELEASE_URL = f"https://github.com/tectonic-typesetting/tectonic/releases/download/tectonic%40{TECTONIC_VERSION}"
def get_target_triple() -> str:
"""Return the Tauri target triple for the current platform."""
system = platform.system().lower()
machine = platform.machine().lower()
if system == "darwin":
if machine == "arm64":
return "aarch64-apple-darwin"
return "x86_64-apple-darwin"
elif system == "windows":
if machine == "amd64" or machine == "x86_64":
return "x86_64-pc-windows-msvc"
return "i686-pc-windows-msvc"
elif system == "linux":
if machine == "aarch64":
return "aarch64-unknown-linux-gnu"
return "x86_64-unknown-linux-gnu"
else:
raise RuntimeError(f"Unsupported platform: {system}/{machine}")
def get_sidecar_name(target_triple: str) -> str:
"""Return the sidecar filename with target triple."""
base = f"spartan-write-sidecar-{target_triple}"
if "windows" in target_triple:
return f"{base}.exe"
return base
def get_tectonic_asset_name(target_triple: str) -> str:
"""Return the tectonic release asset filename for the given target triple."""
# Map Tauri target triples to tectonic asset names
# Note: ARM64 Linux uses musl variant in tectonic releases
asset_map = {
"aarch64-apple-darwin":
f"tectonic-{TECTONIC_VERSION}-aarch64-apple-darwin.tar.gz",
"x86_64-apple-darwin":
f"tectonic-{TECTONIC_VERSION}-x86_64-apple-darwin.tar.gz",
"x86_64-unknown-linux-gnu":
f"tectonic-{TECTONIC_VERSION}-x86_64-unknown-linux-gnu.tar.gz",
"aarch64-unknown-linux-gnu":
f"tectonic-{TECTONIC_VERSION}-aarch64-unknown-linux-musl.tar.gz",
"x86_64-pc-windows-msvc":
f"tectonic-{TECTONIC_VERSION}-x86_64-pc-windows-msvc.zip",
}
if target_triple not in asset_map:
raise RuntimeError(
f"No tectonic binary available for: {target_triple}")
return asset_map[target_triple]
def get_tectonic_sidecar_name(target_triple: str) -> str:
"""Return the tectonic sidecar filename with target triple."""
base = f"tectonic-{target_triple}"
if "windows" in target_triple:
return f"{base}.exe"
return base
def get_npm_executable() -> str:
"""Return the right npm executable for the current OS."""
if os.name == "nt":
# On Windows runners, npm is typically exposed as npm.cmd.
return "npm.cmd"
return "npm"
def download_tectonic(target_triple: str) -> Path:
"""Download tectonic binary and copy to sidecar directory."""
print("\n=== Downloading tectonic binary ===")
asset_name = get_tectonic_asset_name(target_triple)
url = f"{TECTONIC_RELEASE_URL}/{asset_name}"
sidecar_name = get_tectonic_sidecar_name(target_triple)
dest = SIDECAR_BIN_DIR / sidecar_name
print(f"+ Downloading: {url}")
with tempfile.TemporaryDirectory() as tmpdir:
tmppath = Path(tmpdir)
archive_path = tmppath / asset_name
# Download the archive
urllib.request.urlretrieve(url, archive_path)
print(f"+ Downloaded to: {archive_path}")
# Extract the archive
if asset_name.endswith(".tar.gz"):
with tarfile.open(archive_path, "r:gz") as tar:
tar.extractall(tmppath)
binary_name = "tectonic"
elif asset_name.endswith(".zip"):
with zipfile.ZipFile(archive_path, "r") as zf:
zf.extractall(tmppath)
binary_name = "tectonic.exe"
else:
raise RuntimeError(f"Unknown archive format: {asset_name}")
# Find and copy the binary
binary_path = tmppath / binary_name
if not binary_path.exists():
raise FileNotFoundError(
f"Binary not found in archive: {binary_path}")
SIDECAR_BIN_DIR.mkdir(parents=True, exist_ok=True)
shutil.copy2(binary_path, dest)
print(f"+ Copied to: {dest}")
return dest
def run(cmd: list[str],
cwd: Path | None = None,
check: bool = True) -> subprocess.CompletedProcess:
"""Run a command and print it."""
print(f"+ {' '.join(cmd)}")
return subprocess.run(cmd, cwd=cwd, check=check)
def update_submodules() -> None:
"""Fetch and check out the latest submodule commits from each remote."""
print("\n=== Updating git submodules ===")
run(
[
"git",
"submodule",
"update",
"--init",
"--remote",
"--recursive",
],
cwd=ROOT,
)
def build_wheel() -> Path:
"""Build the CLI wheel and return path to the .whl file."""
print("\n=== Building CLI wheel ===")
run(["uv", "build"], cwd=SIDECAR_DIR)
dist_dir = SIDECAR_DIR / "dist"
wheels = list(dist_dir.glob("*.whl"))
if not wheels:
raise FileNotFoundError(f"No .whl file found in {dist_dir}")
wheel = wheels[0]
print(f"+ Built wheel: {wheel}")
return wheel
def build_pyinstaller(target_triple: str) -> Path:
"""Build PyInstaller executable and return path to the binary."""
print("\n=== Building PyInstaller executable ===")
sidecar_name = get_sidecar_name(target_triple)
# Remove extension for PyInstaller --name (it adds .exe on Windows automatically)
exe_name = sidecar_name.removesuffix(".exe")
# PyInstaller output directories
build_dir = SIDECAR_DIR / "build"
dist_dir = SIDECAR_DIR / "pyinstaller_dist"
# Clean previous PyInstaller output
if build_dir.exists():
shutil.rmtree(build_dir)
if dist_dir.exists():
shutil.rmtree(dist_dir)
copy_metadata = []
# Build the command with hidden imports
cmd = [
"uv",
"run",
"--group",
"dev",
"pyinstaller",
"--onefile",
"--name",
exe_name,
"--distpath",
str(dist_dir),
"--workpath",
str(build_dir),
"--specpath",
str(build_dir),
# Include template files for project creation
"--add-data",
f"{SIDECAR_DIR / 'core' / 'project' / 'templates'}:core/project/templates",
]
for module in copy_metadata:
cmd.extend(["--copy-metadata", module])
cmd.append("api/server.py")
# Run PyInstaller via uv to use the dev dependency
run(cmd, cwd=SIDECAR_DIR)
# Find the built executable
exe_path = dist_dir / sidecar_name
if not exe_path.exists():
raise FileNotFoundError(f"PyInstaller output not found: {exe_path}")
print(f"+ Built executable: {exe_path}")
return exe_path
def copy_sidecar(exe_path: Path, target_triple: str) -> Path:
"""Copy the executable to the Tauri sidecar directory."""
print("\n=== Copying sidecar to Tauri ===")
SIDECAR_BIN_DIR.mkdir(parents=True, exist_ok=True)
sidecar_name = get_sidecar_name(target_triple)
dest = SIDECAR_BIN_DIR / sidecar_name
shutil.copy2(exe_path, dest)
print(f"+ Copied to: {dest}")
return dest
def build_tauri_release():
"""Run tauri build to create the final app bundle."""
print("\n=== Building Tauri app ===")
run([get_npm_executable(), "run", "tauri", "build"], cwd=FRONTEND_DIR)
def build_tauri_debug():
"""Run tauri build with debug flag. Skip DMG to avoid bundle_dmg.sh failures."""
print("\n=== Building Tauri app ===")
run(
[
get_npm_executable(),
"run",
"tauri",
"build",
"--",
"--debug",
"--bundles",
"app",
],
cwd=FRONTEND_DIR,
)
def main():
print("=== Spartan Write build script ===")
update_submodules()
target_triple = get_target_triple()
print(f"+ Target triple: {target_triple}")
tectonic_sidecar = SIDECAR_BIN_DIR / get_tectonic_sidecar_name(
target_triple)
if not tectonic_sidecar.exists():
download_tectonic(target_triple)
# Step 1: Build wheel
build_wheel()
# Step 2: Build PyInstaller executable
exe_path = build_pyinstaller(target_triple)
# Step 3: Copy sidecar to Tauri
copy_sidecar(exe_path, target_triple)
# Step 4: Build Tauri app
if "--debug" in sys.argv:
build_tauri_debug()
else:
build_tauri_release()
print("\n=== Build complete ===")
if __name__ == "__main__":
try:
main()
except subprocess.CalledProcessError as e:
print(f"\n!!! Build failed: command exited with code {e.returncode}",
file=sys.stderr)
sys.exit(1)
except FileNotFoundError as e:
print(f"\n!!! Build failed: {e}", file=sys.stderr)
sys.exit(1)