jac-client 0.2.9__py3-none-any.whl → 0.2.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jac_client/plugin/cli.jac +354 -1
- jac_client/plugin/src/desktop_config.jac +31 -0
- jac_client/plugin/src/impl/desktop_config.impl.jac +191 -0
- jac_client/plugin/src/targets/desktop/sidecar/main.py +144 -0
- jac_client/plugin/src/targets/desktop_target.jac +37 -0
- jac_client/plugin/src/targets/impl/desktop_target.impl.jac +2334 -0
- jac_client/plugin/src/targets/impl/registry.impl.jac +64 -0
- jac_client/plugin/src/targets/impl/web_target.impl.jac +157 -0
- jac_client/plugin/src/targets/register.jac +21 -0
- jac_client/plugin/src/targets/registry.jac +87 -0
- jac_client/plugin/src/targets/web_target.jac +35 -0
- jac_client/tests/test_it_desktop.py +891 -0
- {jac_client-0.2.9.dist-info → jac_client-0.2.10.dist-info}/METADATA +2 -2
- {jac_client-0.2.9.dist-info → jac_client-0.2.10.dist-info}/RECORD +17 -6
- {jac_client-0.2.9.dist-info → jac_client-0.2.10.dist-info}/WHEEL +0 -0
- {jac_client-0.2.9.dist-info → jac_client-0.2.10.dist-info}/entry_points.txt +0 -0
- {jac_client-0.2.9.dist-info → jac_client-0.2.10.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,2334 @@
|
|
|
1
|
+
"""Implementation of DesktopTarget methods."""
|
|
2
|
+
import from pathlib { Path }
|
|
3
|
+
import from typing { Optional, Any }
|
|
4
|
+
import from jaclang.cli.console { console }
|
|
5
|
+
import from jaclang.project.config { get_config }
|
|
6
|
+
import subprocess;
|
|
7
|
+
import json;
|
|
8
|
+
import shutil;
|
|
9
|
+
import os;
|
|
10
|
+
import platform;
|
|
11
|
+
import stat;
|
|
12
|
+
|
|
13
|
+
"""Setup desktop target - scaffold Tauri project structure."""
|
|
14
|
+
impl DesktopTarget.setup(self: DesktopTarget, project_dir: Path) -> None {
|
|
15
|
+
# Define tauri_dir early so we can use it in error handling
|
|
16
|
+
tauri_dir = project_dir / "src-tauri";
|
|
17
|
+
# Create src-tauri directory structure FIRST (before ANY other operations)
|
|
18
|
+
# This ensures the directory exists even if later steps fail
|
|
19
|
+
console.print("\n🖥️ Setting up desktop target (Tauri)", style="bold");
|
|
20
|
+
console.print(f" Project directory: {project_dir}", style="muted");
|
|
21
|
+
# Check if already set up
|
|
22
|
+
if tauri_dir.exists() {
|
|
23
|
+
console.warning("Desktop target already set up. Skipping...");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
# Ensure project directory exists
|
|
27
|
+
if not project_dir.exists() {
|
|
28
|
+
try {
|
|
29
|
+
project_dir.mkdir(parents=True, exist_ok=True);
|
|
30
|
+
} except Exception as e {
|
|
31
|
+
raise RuntimeError(f"Failed to create project directory: {e}") ;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
# Create src-tauri directory structure IMMEDIATELY (before imports or any other operations)
|
|
35
|
+
console.print(" Creating src-tauri/ directory structure...", style="muted");
|
|
36
|
+
try {
|
|
37
|
+
tauri_dir.mkdir(parents=True, exist_ok=True);
|
|
38
|
+
(tauri_dir / "src").mkdir(exist_ok=True);
|
|
39
|
+
(tauri_dir / "binaries").mkdir(exist_ok=True);
|
|
40
|
+
console.print(f" ✔ Created {tauri_dir}", style="success");
|
|
41
|
+
} except Exception as e {
|
|
42
|
+
raise RuntimeError(f"Failed to create src-tauri directory: {e}") ;
|
|
43
|
+
}
|
|
44
|
+
# Wrap rest of setup in try-finally to ensure directory exists even if exceptions occur
|
|
45
|
+
try {
|
|
46
|
+
# Now import and do other operations (directory already exists, so if these fail, directory is still there)
|
|
47
|
+
import from jac_client.plugin.src.desktop_config { DesktopConfig }
|
|
48
|
+
|
|
49
|
+
# Load desktop config (will use defaults if [desktop] section doesn't exist)
|
|
50
|
+
# Wrap in try-except to handle config loading errors gracefully
|
|
51
|
+
try {
|
|
52
|
+
desktop_config = DesktopConfig(project_dir=project_dir);
|
|
53
|
+
config_data = desktop_config.load();
|
|
54
|
+
|
|
55
|
+
project_name = config_data.get('name', 'my-jac-app');
|
|
56
|
+
project_version = config_data.get('version', '1.0.0');
|
|
57
|
+
identifier = config_data.get('identifier', 'com.myapp');
|
|
58
|
+
} except Exception as e {
|
|
59
|
+
console.warning(f" Failed to load desktop config: {e}, using defaults");
|
|
60
|
+
project_name = 'my-jac-app';
|
|
61
|
+
project_version = '1.0.0';
|
|
62
|
+
identifier = 'com.myapp';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.print(
|
|
66
|
+
f" Project name: {project_name}, version: {project_version}",
|
|
67
|
+
style="muted"
|
|
68
|
+
);
|
|
69
|
+
console.print(f" Identifier: {identifier}", style="muted");
|
|
70
|
+
|
|
71
|
+
# Generate tauri.conf.json (don't fail setup if this fails)
|
|
72
|
+
try {
|
|
73
|
+
_generate_tauri_config(
|
|
74
|
+
tauri_dir, project_name, identifier, project_version
|
|
75
|
+
);
|
|
76
|
+
} except Exception as e {
|
|
77
|
+
console.warning(f" Failed to generate tauri.conf.json: {e}");
|
|
78
|
+
console.print(
|
|
79
|
+
" Setup will continue, but tauri.conf.json may be missing.",
|
|
80
|
+
style="muted"
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Generate Cargo.toml (don't fail setup if this fails)
|
|
85
|
+
try {
|
|
86
|
+
_generate_cargo_toml(tauri_dir, project_name, identifier);
|
|
87
|
+
} except Exception as e {
|
|
88
|
+
console.warning(f" Failed to generate Cargo.toml: {e}");
|
|
89
|
+
console.print(
|
|
90
|
+
" Setup will continue, but Cargo.toml may be missing.", style="muted"
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Generate build.rs (don't fail setup if this fails)
|
|
95
|
+
try {
|
|
96
|
+
_generate_build_rs(tauri_dir);
|
|
97
|
+
} except Exception as e {
|
|
98
|
+
console.warning(f" Failed to generate build.rs: {e}");
|
|
99
|
+
console.print(
|
|
100
|
+
" Setup will continue, but build.rs may be missing.", style="muted"
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Generate icons (don't fail setup if this fails)
|
|
105
|
+
try {
|
|
106
|
+
_generate_default_icons(tauri_dir);
|
|
107
|
+
} except Exception as e {
|
|
108
|
+
console.warning(f" Failed to generate icons: {e}");
|
|
109
|
+
console.print(
|
|
110
|
+
" Setup will continue, but icons may be missing.", style="muted"
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Update tauri.conf.json to include icons
|
|
115
|
+
try {
|
|
116
|
+
config_path = tauri_dir / "tauri.conf.json";
|
|
117
|
+
if config_path.exists() {
|
|
118
|
+
with open(config_path, "r") as f {
|
|
119
|
+
config = json.load(f);
|
|
120
|
+
}
|
|
121
|
+
_populate_icon_array(tauri_dir, config);
|
|
122
|
+
with open(config_path, "w") as f {
|
|
123
|
+
json.dump(config, f, indent=2);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} except Exception as e {
|
|
127
|
+
console.warning(f" Failed to update icon array in tauri.conf.json: {e}");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Generate main.rs (don't fail setup if this fails)
|
|
131
|
+
try {
|
|
132
|
+
_generate_main_rs(tauri_dir);
|
|
133
|
+
} except Exception as e {
|
|
134
|
+
console.warning(f" Failed to generate main.rs: {e}");
|
|
135
|
+
console.print(
|
|
136
|
+
" Setup will continue, but main.rs may be missing.", style="muted"
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Add [desktop] section to jac.toml (don't fail setup if this fails)
|
|
141
|
+
try {
|
|
142
|
+
_add_desktop_config(project_dir, project_name, identifier, project_version);
|
|
143
|
+
} except Exception as e {
|
|
144
|
+
console.warning(f" Failed to add [desktop] section to jac.toml: {e}");
|
|
145
|
+
console.print(
|
|
146
|
+
" Setup will continue, but jac.toml may not be updated.",
|
|
147
|
+
style="muted"
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# Check and install required dependencies (wrap in try-except to not fail setup)
|
|
152
|
+
console.print("\n📦 Checking required dependencies...", style="bold");
|
|
153
|
+
try {
|
|
154
|
+
_check_and_install_dependencies();
|
|
155
|
+
} except Exception as e {
|
|
156
|
+
console.warning(f" Dependency check encountered an issue: {e}");
|
|
157
|
+
console.print(
|
|
158
|
+
" Setup will continue, but some dependencies may be missing.",
|
|
159
|
+
style="muted"
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# Verify essential files exist, recreate if missing
|
|
164
|
+
essential_files = {
|
|
165
|
+
"tauri.conf.json": lambda :
|
|
166
|
+
_generate_tauri_config(
|
|
167
|
+
tauri_dir, project_name, identifier, project_version
|
|
168
|
+
),
|
|
169
|
+
"Cargo.toml": lambda :
|
|
170
|
+
_generate_cargo_toml(tauri_dir, project_name, identifier),
|
|
171
|
+
"build.rs": lambda : _generate_build_rs(tauri_dir),
|
|
172
|
+
"src/main.rs": lambda : _generate_main_rs(tauri_dir)
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
for file_path in essential_files {
|
|
176
|
+
full_path = tauri_dir / file_path;
|
|
177
|
+
if not full_path.exists() {
|
|
178
|
+
console.warning(f" {file_path} not found, regenerating...");
|
|
179
|
+
try {
|
|
180
|
+
generator = essential_files[file_path];
|
|
181
|
+
generator();
|
|
182
|
+
} except Exception as e {
|
|
183
|
+
console.warning(f" Failed to regenerate {file_path}: {e}");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.success("Desktop target setup complete!");
|
|
189
|
+
console.print("\nNext steps:", style="bold");
|
|
190
|
+
console.print(" 1. Build: jac build main.jac --client desktop");
|
|
191
|
+
console.print(" 2. Dev: jac start main.jac --client desktop");
|
|
192
|
+
} finally {
|
|
193
|
+
# ABSOLUTE GUARANTEE: Ensure src-tauri directory exists no matter what
|
|
194
|
+
if not tauri_dir.exists() {
|
|
195
|
+
try {
|
|
196
|
+
tauri_dir.mkdir(parents=True, exist_ok=True);
|
|
197
|
+
(tauri_dir / "src").mkdir(exist_ok=True);
|
|
198
|
+
(tauri_dir / "binaries").mkdir(exist_ok=True);
|
|
199
|
+
console.print(f" ✔ Ensured {tauri_dir} exists", style="success");
|
|
200
|
+
} except Exception as e {
|
|
201
|
+
console.error(f" CRITICAL: Could not create src-tauri directory: {e}");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
"""Generate identifier from project name."""
|
|
208
|
+
def _generate_identifier(name: str) -> str {
|
|
209
|
+
# Convert to lowercase, replace spaces/special chars with dots
|
|
210
|
+
identifier = name.lower();
|
|
211
|
+
identifier = identifier.replace(" ", ".");
|
|
212
|
+
identifier = identifier.replace("_", ".");
|
|
213
|
+
identifier = identifier.replace("-", ".");
|
|
214
|
+
# Remove invalid characters (keep only alphanumeric and dots)
|
|
215
|
+
filtered = "";
|
|
216
|
+
for char in identifier {
|
|
217
|
+
if char.isalnum() or char == "." {
|
|
218
|
+
filtered += char;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
identifier = filtered;
|
|
222
|
+
# Ensure it starts with a letter
|
|
223
|
+
if identifier and not identifier[0].isalpha() {
|
|
224
|
+
identifier = "com." + identifier;
|
|
225
|
+
}
|
|
226
|
+
# Default if empty
|
|
227
|
+
if not identifier {
|
|
228
|
+
identifier = "com.example.myapp";
|
|
229
|
+
}
|
|
230
|
+
return identifier;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
"""Generate tauri.conf.json."""
|
|
234
|
+
def _generate_tauri_config(
|
|
235
|
+
tauri_dir: Path, name: str, identifier: str, version: str
|
|
236
|
+
) -> None {
|
|
237
|
+
config = {
|
|
238
|
+
"productName": name,
|
|
239
|
+
"version": version,
|
|
240
|
+
"identifier": identifier,
|
|
241
|
+
"build": {
|
|
242
|
+
"devUrl": "http://localhost:5173",
|
|
243
|
+
"frontendDist": "../.jac/client/dist"
|
|
244
|
+
},
|
|
245
|
+
"app": {
|
|
246
|
+
"windows": [
|
|
247
|
+
{
|
|
248
|
+
"title": name,
|
|
249
|
+
"width": 1200,
|
|
250
|
+
"height": 800,
|
|
251
|
+
"minWidth": 800,
|
|
252
|
+
"minHeight": 600,
|
|
253
|
+
"resizable": True,
|
|
254
|
+
"fullscreen": False
|
|
255
|
+
}
|
|
256
|
+
],
|
|
257
|
+
"security": {"csp": None}
|
|
258
|
+
},
|
|
259
|
+
"bundle": {"active": True, "targets": "all", "icon": []},
|
|
260
|
+
"plugins": {}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
config_path = tauri_dir / "tauri.conf.json";
|
|
264
|
+
with open(config_path, "w") as f {
|
|
265
|
+
json.dump(config, f, indent=2);
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
rel_path = config_path.relative_to(tauri_dir.parent);
|
|
269
|
+
console.print(f" ✔ Generated {rel_path}", style="success");
|
|
270
|
+
} except ValueError {
|
|
271
|
+
console.print(f" ✔ Generated {config_path.name}", style="success");
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
"""Generate Cargo.toml."""
|
|
276
|
+
def _generate_cargo_toml(tauri_dir: Path, name: str, identifier: str) -> None {
|
|
277
|
+
# Sanitize name for Cargo (Rust package names)
|
|
278
|
+
cargo_name = name.lower().replace(" ", "-").replace("_", "-");
|
|
279
|
+
cargo_name = "".join(c if c.isalnum() or c == "-" else "" for c in cargo_name);
|
|
280
|
+
if not cargo_name {
|
|
281
|
+
cargo_name = "my-jac-app";
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
cargo_toml = f'''[package]
|
|
285
|
+
name = "{cargo_name}"
|
|
286
|
+
version = "0.1.0"
|
|
287
|
+
description = "A Tauri desktop app built with Jac"
|
|
288
|
+
authors = ["you"]
|
|
289
|
+
license = ""
|
|
290
|
+
repository = ""
|
|
291
|
+
edition = "2021"
|
|
292
|
+
|
|
293
|
+
[build-dependencies]
|
|
294
|
+
tauri-build = {{ version = "2.0", features = [] }}
|
|
295
|
+
|
|
296
|
+
[dependencies]
|
|
297
|
+
tauri = {{ version = "2.0", features = [] }}
|
|
298
|
+
serde = {{ version = "1", features = ["derive"] }}
|
|
299
|
+
serde_json = "1"
|
|
300
|
+
|
|
301
|
+
[features]
|
|
302
|
+
# This feature is used for production builds or when `devPath` points to the filesystem
|
|
303
|
+
custom-protocol = ["tauri/custom-protocol"]
|
|
304
|
+
''';
|
|
305
|
+
|
|
306
|
+
cargo_path = tauri_dir / "Cargo.toml";
|
|
307
|
+
with open(cargo_path, "w") as f {
|
|
308
|
+
f.write(cargo_toml);
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
rel_path = cargo_path.relative_to(tauri_dir.parent);
|
|
312
|
+
console.print(f" ✔ Generated {rel_path}", style="success");
|
|
313
|
+
} except ValueError {
|
|
314
|
+
console.print(f" ✔ Generated {cargo_path.name}", style="success");
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
"""Generate build.rs (required for Tauri v2)."""
|
|
319
|
+
def _generate_build_rs(tauri_dir: Path) -> None {
|
|
320
|
+
build_rs = '''fn main() {
|
|
321
|
+
tauri_build::build()
|
|
322
|
+
}
|
|
323
|
+
''';
|
|
324
|
+
|
|
325
|
+
build_path = tauri_dir / "build.rs";
|
|
326
|
+
with open(build_path, "w") as f {
|
|
327
|
+
f.write(build_rs);
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
rel_path = build_path.relative_to(tauri_dir.parent);
|
|
331
|
+
console.print(f" ✔ Generated {rel_path}", style="success");
|
|
332
|
+
} except ValueError {
|
|
333
|
+
console.print(f" ✔ Generated build.rs", style="success");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
"""Generate default placeholder icons for Tauri."""
|
|
338
|
+
def _generate_default_icons(tauri_dir: Path) -> None {
|
|
339
|
+
icons_dir = tauri_dir / "icons";
|
|
340
|
+
icons_dir.mkdir(exist_ok=True);
|
|
341
|
+
|
|
342
|
+
# Create a 1024x1024 PNG icon as placeholder (AppImage requires square icons, 1024x1024 recommended)
|
|
343
|
+
# Try using PIL/Pillow first (most common), then fallback to subprocess with ImageMagick/convert,
|
|
344
|
+
# finally use a simple Python-based PNG generator
|
|
345
|
+
icon_path = icons_dir / "icon.png";
|
|
346
|
+
|
|
347
|
+
# Method 1: Try using Python with PIL to generate icon
|
|
348
|
+
try {
|
|
349
|
+
import subprocess;
|
|
350
|
+
# Use Python to create a 1024x1024 PNG using PIL if available
|
|
351
|
+
python_code = '''
|
|
352
|
+
import sys
|
|
353
|
+
try:
|
|
354
|
+
from PIL import Image, ImageDraw
|
|
355
|
+
size = 1024
|
|
356
|
+
img = Image.new("RGBA", (size, size), color=(66, 139, 202, 255))
|
|
357
|
+
draw = ImageDraw.Draw(img)
|
|
358
|
+
draw.rectangle([20, 20, size-20, size-20], outline=(255, 255, 255, 255), width=10)
|
|
359
|
+
img.save(sys.argv[1], "PNG")
|
|
360
|
+
sys.exit(0)
|
|
361
|
+
except ImportError:
|
|
362
|
+
sys.exit(1)
|
|
363
|
+
''';
|
|
364
|
+
result = subprocess.run(
|
|
365
|
+
["python3", "-c", python_code, str(icon_path)],
|
|
366
|
+
capture_output=True,
|
|
367
|
+
check=True,
|
|
368
|
+
timeout=5
|
|
369
|
+
);
|
|
370
|
+
console.print(" ✔ Generated default icon (1024x1024)", style="success");
|
|
371
|
+
console.warning(
|
|
372
|
+
" Note: Replace icons/icon.png with your app icon (1024x1024 PNG recommended)"
|
|
373
|
+
);
|
|
374
|
+
return;
|
|
375
|
+
} except (
|
|
376
|
+
subprocess.CalledProcessError,
|
|
377
|
+
FileNotFoundError,
|
|
378
|
+
subprocess.TimeoutExpired
|
|
379
|
+
) {
|
|
380
|
+
# PIL not available, try next method
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
# Method 2: Try ImageMagick convert command
|
|
384
|
+
try {
|
|
385
|
+
import subprocess;
|
|
386
|
+
# Create a 1024x1024 solid blue PNG with alpha channel using ImageMagick
|
|
387
|
+
result = subprocess.run(
|
|
388
|
+
[
|
|
389
|
+
"convert",
|
|
390
|
+
"-size",
|
|
391
|
+
"1024x1024",
|
|
392
|
+
"xc:#428BCA",
|
|
393
|
+
"-alpha",
|
|
394
|
+
"set",
|
|
395
|
+
"-channel",
|
|
396
|
+
"RGBA",
|
|
397
|
+
str(icon_path)
|
|
398
|
+
],
|
|
399
|
+
capture_output=True,
|
|
400
|
+
check=True,
|
|
401
|
+
timeout=5
|
|
402
|
+
);
|
|
403
|
+
console.print(" ✔ Generated default icon (1024x1024)", style="success");
|
|
404
|
+
console.warning(
|
|
405
|
+
" Note: Replace icons/icon.png with your app icon (1024x1024 PNG recommended)"
|
|
406
|
+
);
|
|
407
|
+
return;
|
|
408
|
+
} except (
|
|
409
|
+
subprocess.CalledProcessError,
|
|
410
|
+
FileNotFoundError,
|
|
411
|
+
subprocess.TimeoutExpired
|
|
412
|
+
) {
|
|
413
|
+
# ImageMagick not available, try next method
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
# Method 3: Fallback - create a minimal but valid 1024x1024 PNG using Python
|
|
417
|
+
# This creates a simple solid color PNG
|
|
418
|
+
import struct;
|
|
419
|
+
import zlib;
|
|
420
|
+
|
|
421
|
+
width = 1024;
|
|
422
|
+
height = 1024;
|
|
423
|
+
|
|
424
|
+
# PNG signature
|
|
425
|
+
png = bytearray([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
|
|
426
|
+
|
|
427
|
+
# IHDR chunk - RGBA, 8-bit per channel (color type 6 = RGBA)
|
|
428
|
+
ihdr_data = struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0);
|
|
429
|
+
ihdr_crc = zlib.crc32(b"IHDR" + ihdr_data) & 0xffffffff;
|
|
430
|
+
png.extend(struct.pack(">I", 13));
|
|
431
|
+
png.extend(b"IHDR");
|
|
432
|
+
png.extend(ihdr_data);
|
|
433
|
+
png.extend(struct.pack(">I", ihdr_crc));
|
|
434
|
+
|
|
435
|
+
# IDAT chunk - solid blue color with alpha (RGBA: 66, 139, 202, 255 = #428BCA)
|
|
436
|
+
# PNG scanlines: each row is prefixed with filter byte (0 = none)
|
|
437
|
+
row_size = width * 4; # RGBA = 4 bytes per pixel
|
|
438
|
+
scanline = bytes([0]) + bytes([66, 139, 202, 255]) * width; # Filter byte + RGBA data
|
|
439
|
+
image_data = scanline * height;
|
|
440
|
+
|
|
441
|
+
# Compress
|
|
442
|
+
compressed = zlib.compress(image_data, level=9);
|
|
443
|
+
idat_crc = zlib.crc32(b"IDAT" + compressed) & 0xffffffff;
|
|
444
|
+
png.extend(struct.pack(">I", len(compressed)));
|
|
445
|
+
png.extend(b"IDAT");
|
|
446
|
+
png.extend(compressed);
|
|
447
|
+
png.extend(struct.pack(">I", idat_crc));
|
|
448
|
+
|
|
449
|
+
# IEND chunk
|
|
450
|
+
png.extend(struct.pack(">I", 0));
|
|
451
|
+
png.extend(b"IEND");
|
|
452
|
+
png.extend(struct.pack(">I", 0xAE426082));
|
|
453
|
+
|
|
454
|
+
# Write icon.png
|
|
455
|
+
with open(icon_path, "wb") as f {
|
|
456
|
+
f.write(png);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
console.print(" ✔ Generated default icon (1024x1024)", style="success");
|
|
460
|
+
console.warning(
|
|
461
|
+
" Note: Replace icons/icon.png with your app icon (1024x1024 PNG recommended)"
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
"""Generate main.rs."""
|
|
466
|
+
def _generate_main_rs(tauri_dir: Path) -> None {
|
|
467
|
+
main_rs = '''// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
|
468
|
+
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
|
469
|
+
|
|
470
|
+
use std::process::{Command, Child};
|
|
471
|
+
use std::sync::Mutex;
|
|
472
|
+
use tauri::Manager;
|
|
473
|
+
|
|
474
|
+
// Global storage for sidecar process
|
|
475
|
+
static SIDECAR_PROCESS: Mutex<Option<Child>> = Mutex::new(None);
|
|
476
|
+
|
|
477
|
+
fn find_and_start_sidecar(app: &tauri::AppHandle) -> Result<(), Box<dyn std::error::Error>> {
|
|
478
|
+
// Try to find the sidecar in bundled resources
|
|
479
|
+
let resource_dir = app.path().resource_dir()?;
|
|
480
|
+
|
|
481
|
+
// Possible sidecar names
|
|
482
|
+
let sidecar_names = if cfg!(windows) {
|
|
483
|
+
vec!["binaries/jac-sidecar.exe", "binaries/jac-sidecar.bat"]
|
|
484
|
+
} else {
|
|
485
|
+
vec!["binaries/jac-sidecar", "binaries/jac-sidecar.sh"]
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
let mut sidecar_path = None;
|
|
489
|
+
for name in &sidecar_names {
|
|
490
|
+
let path = resource_dir.join(name);
|
|
491
|
+
if path.exists() {
|
|
492
|
+
sidecar_path = Some(path);
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// If not found in resources, try relative to executable
|
|
498
|
+
if sidecar_path.is_none() {
|
|
499
|
+
if let Ok(exe_path) = std::env::current_exe() {
|
|
500
|
+
if let Some(exe_dir) = exe_path.parent() {
|
|
501
|
+
let exe_dir = exe_dir.to_path_buf();
|
|
502
|
+
for name in &sidecar_names {
|
|
503
|
+
let path = exe_dir.join(name);
|
|
504
|
+
if path.exists() {
|
|
505
|
+
sidecar_path = Some(path);
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if let Some(sidecar_path) = sidecar_path {
|
|
514
|
+
// Determine module path (try to find main.jac relative to app)
|
|
515
|
+
let module_path = if let Ok(exe_path) = std::env::current_exe() {
|
|
516
|
+
if let Some(exe_dir) = exe_path.parent() {
|
|
517
|
+
// Look for main.jac in parent directories
|
|
518
|
+
let mut current = exe_dir.to_path_buf();
|
|
519
|
+
loop {
|
|
520
|
+
let main_jac = current.join("main.jac");
|
|
521
|
+
if main_jac.exists() {
|
|
522
|
+
break Some(main_jac);
|
|
523
|
+
}
|
|
524
|
+
if !current.pop() {
|
|
525
|
+
break None;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
} else {
|
|
529
|
+
None
|
|
530
|
+
}
|
|
531
|
+
} else {
|
|
532
|
+
None
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
// Build command to start sidecar
|
|
536
|
+
let mut cmd = if cfg!(windows) {
|
|
537
|
+
if sidecar_path.extension().and_then(|s| s.to_str()) == Some("bat") {
|
|
538
|
+
let mut c = Command::new("cmd");
|
|
539
|
+
c.arg("/C");
|
|
540
|
+
c.arg(&sidecar_path);
|
|
541
|
+
c
|
|
542
|
+
} else {
|
|
543
|
+
Command::new(&sidecar_path)
|
|
544
|
+
}
|
|
545
|
+
} else {
|
|
546
|
+
if sidecar_path.extension().and_then(|s| s.to_str()) == Some("sh") {
|
|
547
|
+
let mut c = Command::new("sh");
|
|
548
|
+
c.arg(&sidecar_path);
|
|
549
|
+
c
|
|
550
|
+
} else {
|
|
551
|
+
Command::new(&sidecar_path)
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// Add arguments
|
|
556
|
+
if let Some(ref mp) = module_path {
|
|
557
|
+
cmd.arg("--module-path").arg(mp);
|
|
558
|
+
} else {
|
|
559
|
+
cmd.arg("--module-path").arg("main.jac");
|
|
560
|
+
}
|
|
561
|
+
cmd.arg("--port").arg("8000");
|
|
562
|
+
cmd.arg("--host").arg("127.0.0.1");
|
|
563
|
+
|
|
564
|
+
// Spawn sidecar process
|
|
565
|
+
match cmd.spawn() {
|
|
566
|
+
Ok(child) => {
|
|
567
|
+
let mut process = SIDECAR_PROCESS.lock().unwrap();
|
|
568
|
+
*process = Some(child);
|
|
569
|
+
eprintln!("Sidecar started successfully");
|
|
570
|
+
Ok(())
|
|
571
|
+
}
|
|
572
|
+
Err(e) => {
|
|
573
|
+
eprintln!("Failed to start sidecar: {}", e);
|
|
574
|
+
Err(Box::new(e))
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
} else {
|
|
578
|
+
eprintln!("Sidecar not found in resources, skipping auto-start");
|
|
579
|
+
Ok(())
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
fn stop_sidecar() {
|
|
584
|
+
let mut process = SIDECAR_PROCESS.lock().unwrap();
|
|
585
|
+
if let Some(mut child) = process.take() {
|
|
586
|
+
let _ = child.kill();
|
|
587
|
+
let _ = child.wait();
|
|
588
|
+
eprintln!("Sidecar stopped");
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
fn main() {
|
|
593
|
+
tauri::Builder::default()
|
|
594
|
+
.setup(|app| {
|
|
595
|
+
// Try to start sidecar on app startup
|
|
596
|
+
if let Err(e) = find_and_start_sidecar(app.handle()) {
|
|
597
|
+
eprintln!("Warning: Could not start sidecar: {}", e);
|
|
598
|
+
}
|
|
599
|
+
Ok(())
|
|
600
|
+
})
|
|
601
|
+
.on_window_event(|_window, event| {
|
|
602
|
+
// Clean up sidecar when last window closes
|
|
603
|
+
if matches!(event, tauri::WindowEvent::CloseRequested { .. }) {
|
|
604
|
+
stop_sidecar();
|
|
605
|
+
}
|
|
606
|
+
})
|
|
607
|
+
.run(tauri::generate_context!())
|
|
608
|
+
.expect("error while running tauri application");
|
|
609
|
+
|
|
610
|
+
// Ensure sidecar is stopped on exit
|
|
611
|
+
stop_sidecar();
|
|
612
|
+
}
|
|
613
|
+
''';
|
|
614
|
+
|
|
615
|
+
main_path = tauri_dir / "src" / "main.rs";
|
|
616
|
+
with open(main_path, "w") as f {
|
|
617
|
+
f.write(main_rs);
|
|
618
|
+
}
|
|
619
|
+
try {
|
|
620
|
+
rel_path = main_path.relative_to(tauri_dir.parent);
|
|
621
|
+
console.print(f" ✔ Generated {rel_path}", style="success");
|
|
622
|
+
} except ValueError {
|
|
623
|
+
console.print(f" ✔ Generated src/main.rs", style="success");
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
"""Add [desktop] section to jac.toml."""
|
|
628
|
+
def _add_desktop_config(
|
|
629
|
+
project_dir: Path, name: str, identifier: str, version: str
|
|
630
|
+
) -> None {
|
|
631
|
+
# Read existing jac.toml
|
|
632
|
+
jac_toml_path = project_dir / "jac.toml";
|
|
633
|
+
if not jac_toml_path.exists() {
|
|
634
|
+
raise RuntimeError("jac.toml not found") ;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
# Read current content
|
|
638
|
+
with open(jac_toml_path, "r") as f {
|
|
639
|
+
content = f.read();
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
# Check if [desktop] section already exists
|
|
643
|
+
if "[desktop]" in content {
|
|
644
|
+
console.warning(" [desktop] section already exists in jac.toml. Skipping...");
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
# Append [desktop] section
|
|
649
|
+
desktop_section = f'''
|
|
650
|
+
|
|
651
|
+
# Desktop target configuration (Tauri)
|
|
652
|
+
[desktop]
|
|
653
|
+
name = "{name}"
|
|
654
|
+
identifier = "{identifier}"
|
|
655
|
+
version = "{version}"
|
|
656
|
+
|
|
657
|
+
[desktop.window]
|
|
658
|
+
title = "{name}"
|
|
659
|
+
width = 1200
|
|
660
|
+
height = 800
|
|
661
|
+
min_width = 800
|
|
662
|
+
min_height = 600
|
|
663
|
+
resizable = true
|
|
664
|
+
fullscreen = false
|
|
665
|
+
|
|
666
|
+
[desktop.platforms]
|
|
667
|
+
windows = true
|
|
668
|
+
macos = true
|
|
669
|
+
linux = true
|
|
670
|
+
|
|
671
|
+
[desktop.features]
|
|
672
|
+
system_tray = false
|
|
673
|
+
auto_update = false
|
|
674
|
+
notifications = false
|
|
675
|
+
''';
|
|
676
|
+
|
|
677
|
+
# Append the section
|
|
678
|
+
with open(jac_toml_path, "a") as f {
|
|
679
|
+
f.write(desktop_section);
|
|
680
|
+
}
|
|
681
|
+
console.print(" ✔ Added [desktop] section to jac.toml", style="success");
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
"""Check and install all required dependencies for desktop target."""
|
|
685
|
+
def _check_and_install_dependencies -> None {
|
|
686
|
+
import platform as platform_module;
|
|
687
|
+
import sys;
|
|
688
|
+
|
|
689
|
+
system = platform_module.system();
|
|
690
|
+
|
|
691
|
+
# Check Rust toolchain
|
|
692
|
+
rust_installed = _check_and_install_rust();
|
|
693
|
+
|
|
694
|
+
# Check build tools
|
|
695
|
+
build_tools_installed = _check_and_install_build_tools(system);
|
|
696
|
+
|
|
697
|
+
# Check system dependencies (Linux only)
|
|
698
|
+
if system == "Linux" {
|
|
699
|
+
_check_and_install_linux_dependencies();
|
|
700
|
+
} elif system == "Darwin" {
|
|
701
|
+
_check_macos_dependencies();
|
|
702
|
+
} elif system == "Windows" {
|
|
703
|
+
_check_windows_dependencies();
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
# Check Tauri CLI
|
|
707
|
+
_check_and_install_tauri_cli();
|
|
708
|
+
|
|
709
|
+
# Check Python and jaclang (required for sidecar)
|
|
710
|
+
_check_python_and_jaclang();
|
|
711
|
+
|
|
712
|
+
# Summary
|
|
713
|
+
console.print("\n Dependency check complete!", style="success");
|
|
714
|
+
if not rust_installed {
|
|
715
|
+
console.warning(
|
|
716
|
+
" Rust is required but not installed. Please install it manually."
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
if not build_tools_installed {
|
|
720
|
+
console.warning(
|
|
721
|
+
" Build tools are required but not installed. Please install them manually."
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
"""Check if Rust toolchain is installed, and offer to install if missing."""
|
|
727
|
+
def _check_and_install_rust -> bool {
|
|
728
|
+
try {
|
|
729
|
+
result = subprocess.run(
|
|
730
|
+
["rustc", "--version"], capture_output=True, text=True, timeout=5
|
|
731
|
+
);
|
|
732
|
+
if result.returncode == 0 {
|
|
733
|
+
version = result.stdout.strip();
|
|
734
|
+
console.print(f" ✔ Rust toolchain found: {version}", style="success");
|
|
735
|
+
return True;
|
|
736
|
+
}
|
|
737
|
+
} except Exception { }
|
|
738
|
+
|
|
739
|
+
# Rust not found
|
|
740
|
+
console.warning(" Rust toolchain not found");
|
|
741
|
+
console.print(
|
|
742
|
+
" Rust is required for building desktop applications with Tauri.",
|
|
743
|
+
style="muted"
|
|
744
|
+
);
|
|
745
|
+
console.print(" Install Rust: https://rustup.rs/", style="muted");
|
|
746
|
+
|
|
747
|
+
# Try to detect if we can install automatically
|
|
748
|
+
try {
|
|
749
|
+
# Check if curl is available
|
|
750
|
+
subprocess.run(
|
|
751
|
+
["curl", "--version"], capture_output=True, check=True, timeout=2
|
|
752
|
+
);
|
|
753
|
+
console.print(
|
|
754
|
+
"\n Would you like to install Rust automatically using rustup?",
|
|
755
|
+
style="info"
|
|
756
|
+
);
|
|
757
|
+
response = input(" Install Rust? [Y/n]: ").strip().lower();
|
|
758
|
+
if not response or response in ('y', 'yes') {
|
|
759
|
+
console.print(" Installing Rust...", style="muted");
|
|
760
|
+
console.print(" This may take a few minutes...", style="muted");
|
|
761
|
+
try {
|
|
762
|
+
# Run rustup installer directly via curl pipe
|
|
763
|
+
result = subprocess.run(
|
|
764
|
+
[
|
|
765
|
+
"sh",
|
|
766
|
+
"-c",
|
|
767
|
+
"curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y"
|
|
768
|
+
],
|
|
769
|
+
timeout=600, # 10 minute timeout
|
|
770
|
+
check=False
|
|
771
|
+
);
|
|
772
|
+
if result.returncode == 0 {
|
|
773
|
+
console.print(" ✔ Rust installed successfully!", style="success");
|
|
774
|
+
console.print(
|
|
775
|
+
" Note: You may need to restart your terminal or run: source $HOME/.cargo/env",
|
|
776
|
+
style="muted"
|
|
777
|
+
);
|
|
778
|
+
# Try to source cargo env and verify
|
|
779
|
+
import os;
|
|
780
|
+
cargo_bin = os.path.expanduser("~/.cargo/bin");
|
|
781
|
+
if os.path.exists(cargo_bin) {
|
|
782
|
+
# Add to PATH for current session
|
|
783
|
+
current_path = os.environ.get("PATH", "");
|
|
784
|
+
os.environ["PATH"] = f"{cargo_bin}:{current_path}";
|
|
785
|
+
# Verify installation
|
|
786
|
+
try {
|
|
787
|
+
verify_result = subprocess.run(
|
|
788
|
+
["rustc", "--version"],
|
|
789
|
+
capture_output=True,
|
|
790
|
+
text=True,
|
|
791
|
+
timeout=5
|
|
792
|
+
);
|
|
793
|
+
if verify_result.returncode == 0 {
|
|
794
|
+
version = verify_result.stdout.strip();
|
|
795
|
+
console.print(
|
|
796
|
+
f" ✔ Verified: {version}", style="success"
|
|
797
|
+
);
|
|
798
|
+
return True;
|
|
799
|
+
}
|
|
800
|
+
} except Exception { }
|
|
801
|
+
return True;
|
|
802
|
+
} else {
|
|
803
|
+
console.warning(
|
|
804
|
+
" Rust installation failed. Please install manually."
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
} except Exception as e {
|
|
809
|
+
error_str = str(e);
|
|
810
|
+
if "TimeoutExpired" in error_str or "timeout" in error_str.lower() {
|
|
811
|
+
console.warning(
|
|
812
|
+
" Rust installation timed out. Please install manually."
|
|
813
|
+
);
|
|
814
|
+
} else {
|
|
815
|
+
console.warning(f" Failed to install Rust automatically: {e}");
|
|
816
|
+
console.print(
|
|
817
|
+
" Please install manually: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh",
|
|
818
|
+
style="muted"
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
} else {
|
|
823
|
+
console.print(
|
|
824
|
+
" Skipping Rust installation. Please install manually.", style="muted"
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
} except Exception {
|
|
828
|
+
console.print(
|
|
829
|
+
" curl not found. Cannot install Rust automatically.", style="muted"
|
|
830
|
+
);
|
|
831
|
+
console.print(
|
|
832
|
+
" Please install manually: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh",
|
|
833
|
+
style="muted"
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
return False;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
"""Check and install build tools based on OS."""
|
|
841
|
+
def _check_and_install_build_tools(system: str) -> bool {
|
|
842
|
+
if system == "Linux" {
|
|
843
|
+
# Check for gcc/cc
|
|
844
|
+
try {
|
|
845
|
+
subprocess.run(
|
|
846
|
+
["gcc", "--version"], capture_output=True, check=True, timeout=5
|
|
847
|
+
);
|
|
848
|
+
console.print(" ✔ Build tools (gcc) found", style="success");
|
|
849
|
+
return True;
|
|
850
|
+
} except Exception {
|
|
851
|
+
console.warning(" Build tools (gcc) not found");
|
|
852
|
+
_try_install_linux_build_tools();
|
|
853
|
+
return False;
|
|
854
|
+
}
|
|
855
|
+
} elif system == "Darwin" {
|
|
856
|
+
# Check for Xcode Command Line Tools
|
|
857
|
+
try {
|
|
858
|
+
result = subprocess.run(
|
|
859
|
+
["xcode-select", "-p"], capture_output=True, text=True, timeout=5
|
|
860
|
+
);
|
|
861
|
+
if result.returncode == 0 {
|
|
862
|
+
console.print(" ✔ Xcode Command Line Tools found", style="success");
|
|
863
|
+
return True;
|
|
864
|
+
}
|
|
865
|
+
} except Exception { }
|
|
866
|
+
console.warning(" Xcode Command Line Tools not found");
|
|
867
|
+
console.print(" Install with: xcode-select --install", style="muted");
|
|
868
|
+
console.print("\n Would you like to open the installer?", style="info");
|
|
869
|
+
response = input(" Open installer? [Y/n]: ").strip().lower();
|
|
870
|
+
if not response or response in ('y', 'yes') {
|
|
871
|
+
try {
|
|
872
|
+
subprocess.run(["xcode-select", "--install"], check=False);
|
|
873
|
+
console.print(
|
|
874
|
+
" ✔ Installer opened. Please complete the installation.",
|
|
875
|
+
style="success"
|
|
876
|
+
);
|
|
877
|
+
} except Exception {
|
|
878
|
+
console.warning(
|
|
879
|
+
" Failed to open installer. Please run: xcode-select --install"
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
return False;
|
|
884
|
+
} elif system == "Windows" {
|
|
885
|
+
# Check for Visual Studio Build Tools
|
|
886
|
+
console.warning(" Build tools check not implemented for Windows");
|
|
887
|
+
console.print(
|
|
888
|
+
" Please install Visual Studio Build Tools manually:", style="muted"
|
|
889
|
+
);
|
|
890
|
+
console.print(
|
|
891
|
+
" https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022",
|
|
892
|
+
style="muted"
|
|
893
|
+
);
|
|
894
|
+
return False;
|
|
895
|
+
}
|
|
896
|
+
return False;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
"""Try to install Linux build tools."""
|
|
900
|
+
def _try_install_linux_build_tools -> None {
|
|
901
|
+
import platform as platform_module;
|
|
902
|
+
import os;
|
|
903
|
+
|
|
904
|
+
# Detect Linux distribution
|
|
905
|
+
distro = _detect_linux_distro();
|
|
906
|
+
|
|
907
|
+
if not distro {
|
|
908
|
+
console.print(
|
|
909
|
+
" Could not detect Linux distribution. Please install build tools manually.",
|
|
910
|
+
style="muted"
|
|
911
|
+
);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
console.print(f" Detected: {distro}", style="muted");
|
|
916
|
+
console.print(
|
|
917
|
+
"\n Would you like to install build tools automatically?", style="info"
|
|
918
|
+
);
|
|
919
|
+
response = input(" Install build tools? [Y/n]: ").strip().lower();
|
|
920
|
+
if not response or response in ('y', 'yes') {
|
|
921
|
+
try {
|
|
922
|
+
if distro in ("ubuntu", "debian") {
|
|
923
|
+
console.print(" Installing build-essential...", style="muted");
|
|
924
|
+
result = subprocess.run(
|
|
925
|
+
["sudo", "apt-get", "update"], check=False, timeout=60
|
|
926
|
+
);
|
|
927
|
+
if result.returncode == 0 {
|
|
928
|
+
result = subprocess.run(
|
|
929
|
+
["sudo", "apt-get", "install", "-y", "build-essential"],
|
|
930
|
+
check=False,
|
|
931
|
+
timeout=300
|
|
932
|
+
);
|
|
933
|
+
if result.returncode == 0 {
|
|
934
|
+
console.print(" ✔ Build tools installed", style="success");
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
} elif distro == "fedora" {
|
|
939
|
+
console.print(" Installing gcc gcc-c++...", style="muted");
|
|
940
|
+
result = subprocess.run(
|
|
941
|
+
["sudo", "dnf", "install", "-y", "gcc", "gcc-c++"],
|
|
942
|
+
check=False,
|
|
943
|
+
timeout=300
|
|
944
|
+
);
|
|
945
|
+
if result.returncode == 0 {
|
|
946
|
+
console.print(" ✔ Build tools installed", style="success");
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
} elif distro == "arch" {
|
|
950
|
+
console.print(" Installing base-devel...", style="muted");
|
|
951
|
+
result = subprocess.run(
|
|
952
|
+
["sudo", "pacman", "-S", "--noconfirm", "base-devel"],
|
|
953
|
+
check=False,
|
|
954
|
+
timeout=300
|
|
955
|
+
);
|
|
956
|
+
if result.returncode == 0 {
|
|
957
|
+
console.print(" ✔ Build tools installed", style="success");
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
console.warning(" Failed to install build tools automatically");
|
|
962
|
+
} except Exception as e {
|
|
963
|
+
console.warning(f" Error installing build tools: {e}");
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
"""Detect Linux distribution."""
|
|
969
|
+
def _detect_linux_distro -> str | None {
|
|
970
|
+
import os;
|
|
971
|
+
try {
|
|
972
|
+
# Check /etc/os-release
|
|
973
|
+
if os.path.exists("/etc/os-release") {
|
|
974
|
+
with open("/etc/os-release", "r") as f {
|
|
975
|
+
content = f.read().lower();
|
|
976
|
+
if "ubuntu" in content or "debian" in content {
|
|
977
|
+
if "ubuntu" in content {
|
|
978
|
+
return "ubuntu";
|
|
979
|
+
}
|
|
980
|
+
return "debian";
|
|
981
|
+
} elif "fedora" in content {
|
|
982
|
+
return "fedora";
|
|
983
|
+
} elif "arch" in content or "manjaro" in content {
|
|
984
|
+
return "arch";
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
} except Exception {
|
|
989
|
+
return None;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
"""Check and install Linux system dependencies."""
|
|
994
|
+
def _check_and_install_linux_dependencies -> None {
|
|
995
|
+
# Check pkg-config
|
|
996
|
+
pkg_config_ok = False;
|
|
997
|
+
try {
|
|
998
|
+
subprocess.run(
|
|
999
|
+
["pkg-config", "--version"], capture_output=True, check=True, timeout=5
|
|
1000
|
+
);
|
|
1001
|
+
console.print(" ✔ pkg-config found", style="success");
|
|
1002
|
+
pkg_config_ok = True;
|
|
1003
|
+
} except Exception {
|
|
1004
|
+
console.warning(" pkg-config not found");
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
# Check for GTK/WebKit libraries
|
|
1008
|
+
webkit_ok = False;
|
|
1009
|
+
try {
|
|
1010
|
+
result = subprocess.run(
|
|
1011
|
+
["pkg-config", "--exists", "webkit2gtk-4.1"],
|
|
1012
|
+
capture_output=True,
|
|
1013
|
+
timeout=5
|
|
1014
|
+
);
|
|
1015
|
+
if result.returncode == 0 {
|
|
1016
|
+
console.print(" ✔ GTK/WebKit libraries found", style="success");
|
|
1017
|
+
webkit_ok = True;
|
|
1018
|
+
}
|
|
1019
|
+
} except Exception {
|
|
1020
|
+
console.warning(" GTK/WebKit libraries not found");
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if pkg_config_ok and webkit_ok {
|
|
1024
|
+
return; # All dependencies satisfied
|
|
1025
|
+
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
# Missing dependencies
|
|
1029
|
+
console.warning(" Some Linux system dependencies are missing");
|
|
1030
|
+
distro = _detect_linux_distro();
|
|
1031
|
+
|
|
1032
|
+
if distro {
|
|
1033
|
+
console.print(f" Detected: {distro}", style="muted");
|
|
1034
|
+
console.print(
|
|
1035
|
+
"\n Would you like to install missing dependencies automatically?",
|
|
1036
|
+
style="info"
|
|
1037
|
+
);
|
|
1038
|
+
response = input(" Install dependencies? [Y/n]: ").strip().lower();
|
|
1039
|
+
if not response or response in ('y', 'yes') {
|
|
1040
|
+
_try_install_linux_system_deps(distro);
|
|
1041
|
+
} else {
|
|
1042
|
+
_print_manual_install_instructions(distro);
|
|
1043
|
+
}
|
|
1044
|
+
} else {
|
|
1045
|
+
_print_manual_install_instructions(None);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
"""Try to install Linux system dependencies."""
|
|
1050
|
+
def _try_install_linux_system_deps(distro: str) -> None {
|
|
1051
|
+
try {
|
|
1052
|
+
if distro in ("ubuntu", "debian") {
|
|
1053
|
+
deps = [
|
|
1054
|
+
"libwebkit2gtk-4.1-dev",
|
|
1055
|
+
"build-essential",
|
|
1056
|
+
"curl",
|
|
1057
|
+
"wget",
|
|
1058
|
+
"libssl-dev",
|
|
1059
|
+
"libgtk-3-dev",
|
|
1060
|
+
"libayatana-appindicator3-dev",
|
|
1061
|
+
"librsvg2-dev"
|
|
1062
|
+
];
|
|
1063
|
+
console.print(" Installing dependencies...", style="muted");
|
|
1064
|
+
result = subprocess.run(
|
|
1065
|
+
["sudo", "apt-get", "update"], check=False, timeout=60
|
|
1066
|
+
);
|
|
1067
|
+
if result.returncode == 0 {
|
|
1068
|
+
result = subprocess.run(
|
|
1069
|
+
["sudo", "apt-get", "install", "-y"] + deps,
|
|
1070
|
+
check=False,
|
|
1071
|
+
timeout=600
|
|
1072
|
+
);
|
|
1073
|
+
if result.returncode == 0 {
|
|
1074
|
+
console.print(" ✔ Dependencies installed", style="success");
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
} elif distro == "fedora" {
|
|
1079
|
+
deps = [
|
|
1080
|
+
"webkit2gtk3-devel.x86_64",
|
|
1081
|
+
"openssl-devel",
|
|
1082
|
+
"curl",
|
|
1083
|
+
"wget",
|
|
1084
|
+
"libappindicator-gtk3",
|
|
1085
|
+
"librsvg2-devel"
|
|
1086
|
+
];
|
|
1087
|
+
console.print(" Installing dependencies...", style="muted");
|
|
1088
|
+
result = subprocess.run(
|
|
1089
|
+
["sudo", "dnf", "install", "-y"] + deps, check=False, timeout=600
|
|
1090
|
+
);
|
|
1091
|
+
if result.returncode == 0 {
|
|
1092
|
+
console.print(" ✔ Dependencies installed", style="success");
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
} elif distro == "arch" {
|
|
1096
|
+
deps = [
|
|
1097
|
+
"webkit2gtk",
|
|
1098
|
+
"base-devel",
|
|
1099
|
+
"curl",
|
|
1100
|
+
"wget",
|
|
1101
|
+
"openssl",
|
|
1102
|
+
"appmenu-gtk-module",
|
|
1103
|
+
"gtk3",
|
|
1104
|
+
"libappindicator-gtk3",
|
|
1105
|
+
"librsvg",
|
|
1106
|
+
"libvips"
|
|
1107
|
+
];
|
|
1108
|
+
console.print(" Installing dependencies...", style="muted");
|
|
1109
|
+
result = subprocess.run(
|
|
1110
|
+
["sudo", "pacman", "-S", "--noconfirm"] + deps,
|
|
1111
|
+
check=False,
|
|
1112
|
+
timeout=600
|
|
1113
|
+
);
|
|
1114
|
+
if result.returncode == 0 {
|
|
1115
|
+
console.print(" ✔ Dependencies installed", style="success");
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
console.warning(" Failed to install dependencies automatically");
|
|
1120
|
+
_print_manual_install_instructions(distro);
|
|
1121
|
+
} except Exception as e {
|
|
1122
|
+
console.warning(f" Error installing dependencies: {e}");
|
|
1123
|
+
_print_manual_install_instructions(distro);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
"""Print manual installation instructions."""
|
|
1128
|
+
def _print_manual_install_instructions(distro: str | None) -> None {
|
|
1129
|
+
console.print(" Install system dependencies manually:", style="muted");
|
|
1130
|
+
if distro in ("ubuntu", "debian") {
|
|
1131
|
+
console.print(
|
|
1132
|
+
" sudo apt-get install libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev",
|
|
1133
|
+
style="muted"
|
|
1134
|
+
);
|
|
1135
|
+
} elif distro == "fedora" {
|
|
1136
|
+
console.print(
|
|
1137
|
+
" sudo dnf install webkit2gtk3-devel.x86_64 openssl-devel curl wget libappindicator-gtk3 librsvg2-devel",
|
|
1138
|
+
style="muted"
|
|
1139
|
+
);
|
|
1140
|
+
} elif distro == "arch" {
|
|
1141
|
+
console.print(
|
|
1142
|
+
" sudo pacman -S webkit2gtk base-devel curl wget openssl appmenu-gtk-module gtk3 libappindicator-gtk3 librsvg libvips",
|
|
1143
|
+
style="muted"
|
|
1144
|
+
);
|
|
1145
|
+
} else {
|
|
1146
|
+
console.print(
|
|
1147
|
+
" Ubuntu/Debian: sudo apt-get install libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev",
|
|
1148
|
+
style="muted"
|
|
1149
|
+
);
|
|
1150
|
+
console.print(
|
|
1151
|
+
" Fedora: sudo dnf install webkit2gtk3-devel.x86_64 openssl-devel curl wget libappindicator-gtk3 librsvg2-devel",
|
|
1152
|
+
style="muted"
|
|
1153
|
+
);
|
|
1154
|
+
console.print(
|
|
1155
|
+
" Arch: sudo pacman -S webkit2gtk base-devel curl wget openssl appmenu-gtk-module gtk3 libappindicator-gtk3 librsvg libvips",
|
|
1156
|
+
style="muted"
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
"""Check macOS dependencies."""
|
|
1162
|
+
def _check_macos_dependencies -> None {
|
|
1163
|
+
# macOS dependencies are typically handled by Xcode Command Line Tools
|
|
1164
|
+
# which is checked in _check_and_install_build_tools
|
|
1165
|
+
console.print(
|
|
1166
|
+
" ✔ macOS dependencies (handled by Xcode Command Line Tools)", style="success"
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
"""Check Windows dependencies."""
|
|
1171
|
+
def _check_windows_dependencies -> None {
|
|
1172
|
+
console.warning(" Windows dependencies check not fully implemented");
|
|
1173
|
+
console.print(
|
|
1174
|
+
" Please ensure Visual Studio Build Tools are installed:", style="muted"
|
|
1175
|
+
);
|
|
1176
|
+
console.print(
|
|
1177
|
+
" https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022",
|
|
1178
|
+
style="muted"
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
"""Check Python and jaclang (required for sidecar functionality)."""
|
|
1183
|
+
def _check_python_and_jaclang -> None {
|
|
1184
|
+
# Check Python - try python3 first, then python
|
|
1185
|
+
python_ok = False;
|
|
1186
|
+
python_cmd = None;
|
|
1187
|
+
python_version = None;
|
|
1188
|
+
|
|
1189
|
+
try {
|
|
1190
|
+
result = subprocess.run(
|
|
1191
|
+
["python3", "--version"], capture_output=True, text=True, timeout=5
|
|
1192
|
+
);
|
|
1193
|
+
if result.returncode == 0 {
|
|
1194
|
+
python_version = result.stdout.strip();
|
|
1195
|
+
console.print(f" ✔ Python found: {python_version}", style="success");
|
|
1196
|
+
python_ok = True;
|
|
1197
|
+
python_cmd = "python3";
|
|
1198
|
+
}
|
|
1199
|
+
} except Exception { }
|
|
1200
|
+
|
|
1201
|
+
# Try python command as fallback if python3 not found
|
|
1202
|
+
if not python_ok {
|
|
1203
|
+
try {
|
|
1204
|
+
result = subprocess.run(
|
|
1205
|
+
["python", "--version"], capture_output=True, text=True, timeout=5
|
|
1206
|
+
);
|
|
1207
|
+
if result.returncode == 0 {
|
|
1208
|
+
python_version = result.stdout.strip();
|
|
1209
|
+
console.print(f" ✔ Python found: {python_version}", style="success");
|
|
1210
|
+
python_ok = True;
|
|
1211
|
+
python_cmd = "python";
|
|
1212
|
+
}
|
|
1213
|
+
} except Exception { }
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
if not python_ok {
|
|
1217
|
+
console.warning(" Python not found");
|
|
1218
|
+
console.print(
|
|
1219
|
+
" Python is required for sidecar functionality (Jac backend).",
|
|
1220
|
+
style="muted"
|
|
1221
|
+
);
|
|
1222
|
+
console.print(
|
|
1223
|
+
" Install Python: https://www.python.org/downloads/", style="muted"
|
|
1224
|
+
);
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
# Check jaclang using the detected python command
|
|
1229
|
+
jaclang_ok = False;
|
|
1230
|
+
try {
|
|
1231
|
+
result = subprocess.run(
|
|
1232
|
+
[python_cmd, "-m", "jaclang", "--version"],
|
|
1233
|
+
capture_output=True,
|
|
1234
|
+
text=True,
|
|
1235
|
+
timeout=5
|
|
1236
|
+
);
|
|
1237
|
+
if result.returncode == 0 {
|
|
1238
|
+
version = result.stdout.strip();
|
|
1239
|
+
console.print(f" ✔ jaclang found: {version}", style="success");
|
|
1240
|
+
jaclang_ok = True;
|
|
1241
|
+
}
|
|
1242
|
+
} except Exception { }
|
|
1243
|
+
|
|
1244
|
+
if not jaclang_ok {
|
|
1245
|
+
console.warning(" jaclang not found");
|
|
1246
|
+
console.print(
|
|
1247
|
+
" jaclang is required for sidecar functionality (Jac backend).",
|
|
1248
|
+
style="muted"
|
|
1249
|
+
);
|
|
1250
|
+
console.print("\n Would you like to install jaclang?", style="info");
|
|
1251
|
+
response = input(" Install jaclang? [Y/n]: ").strip().lower();
|
|
1252
|
+
if not response or response in ('y', 'yes') {
|
|
1253
|
+
console.print(" Installing jaclang...", style="muted");
|
|
1254
|
+
try {
|
|
1255
|
+
result = subprocess.run(
|
|
1256
|
+
[python_cmd, "-m", "pip", "install", "jaclang"],
|
|
1257
|
+
check=False,
|
|
1258
|
+
timeout=300,
|
|
1259
|
+
capture_output=False # Show output
|
|
1260
|
+
);
|
|
1261
|
+
if result.returncode == 0 {
|
|
1262
|
+
console.print(" ✔ jaclang installed", style="success");
|
|
1263
|
+
} else {
|
|
1264
|
+
console.warning(" Failed to install jaclang");
|
|
1265
|
+
console.print(
|
|
1266
|
+
" Please install manually: pip install jaclang", style="muted"
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
} except Exception as e {
|
|
1270
|
+
console.warning(f" Error installing jaclang: {e}");
|
|
1271
|
+
console.print(
|
|
1272
|
+
" Please install manually: pip install jaclang", style="muted"
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
} else {
|
|
1276
|
+
console.print(" Skipping jaclang installation.", style="muted");
|
|
1277
|
+
console.print(
|
|
1278
|
+
" Sidecar will not work without jaclang. Install with: pip install jaclang",
|
|
1279
|
+
style="muted"
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
"""Check and install Tauri CLI if needed."""
|
|
1286
|
+
def _check_and_install_tauri_cli -> None {
|
|
1287
|
+
# Check if cargo tauri is available (preferred method)
|
|
1288
|
+
cargo_tauri_ok = False;
|
|
1289
|
+
try {
|
|
1290
|
+
result = subprocess.run(
|
|
1291
|
+
["cargo", "tauri", "--version"], capture_output=True, text=True, timeout=5
|
|
1292
|
+
);
|
|
1293
|
+
if result.returncode == 0 {
|
|
1294
|
+
version = result.stdout.strip();
|
|
1295
|
+
console.print(f" ✔ Tauri CLI found (cargo): {version}", style="success");
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
} except Exception { }
|
|
1299
|
+
|
|
1300
|
+
# Check if npm tauri CLI is available
|
|
1301
|
+
npm_tauri_ok = False;
|
|
1302
|
+
try {
|
|
1303
|
+
result = subprocess.run(
|
|
1304
|
+
["npm", "list", "-g", "@tauri-apps/cli"],
|
|
1305
|
+
capture_output=True,
|
|
1306
|
+
text=True,
|
|
1307
|
+
timeout=5
|
|
1308
|
+
);
|
|
1309
|
+
if result.returncode == 0 {
|
|
1310
|
+
console.print(" ✔ Tauri CLI found (npm)", style="success");
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
} except Exception { }
|
|
1314
|
+
|
|
1315
|
+
# Tauri CLI not found
|
|
1316
|
+
console.warning(" Tauri CLI not found");
|
|
1317
|
+
console.print(
|
|
1318
|
+
" Tauri CLI is required for building desktop applications.", style="muted"
|
|
1319
|
+
);
|
|
1320
|
+
|
|
1321
|
+
# Check if Rust/Cargo is available (required for cargo install)
|
|
1322
|
+
cargo_available = False;
|
|
1323
|
+
try {
|
|
1324
|
+
subprocess.run(
|
|
1325
|
+
["cargo", "--version"], capture_output=True, check=True, timeout=5
|
|
1326
|
+
);
|
|
1327
|
+
cargo_available = True;
|
|
1328
|
+
} except Exception { }
|
|
1329
|
+
|
|
1330
|
+
# Check if npm is available
|
|
1331
|
+
npm_available = False;
|
|
1332
|
+
try {
|
|
1333
|
+
subprocess.run(
|
|
1334
|
+
["npm", "--version"], capture_output=True, check=True, timeout=5
|
|
1335
|
+
);
|
|
1336
|
+
npm_available = True;
|
|
1337
|
+
} except Exception { }
|
|
1338
|
+
|
|
1339
|
+
if not cargo_available and not npm_available {
|
|
1340
|
+
console.print(
|
|
1341
|
+
" Neither cargo nor npm is available. Cannot install Tauri CLI automatically.",
|
|
1342
|
+
style="muted"
|
|
1343
|
+
);
|
|
1344
|
+
console.print(
|
|
1345
|
+
" Please install Rust (for cargo) or Node.js (for npm) first.",
|
|
1346
|
+
style="muted"
|
|
1347
|
+
);
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
# Offer to install
|
|
1352
|
+
console.print("\n Would you like to install Tauri CLI?", style="info");
|
|
1353
|
+
response = input(" Install Tauri CLI? [Y/n]: ").strip().lower();
|
|
1354
|
+
if not response or response in ('y', 'yes') {
|
|
1355
|
+
if cargo_available {
|
|
1356
|
+
console.print(" Installing Tauri CLI via cargo...", style="muted");
|
|
1357
|
+
try {
|
|
1358
|
+
result = subprocess.run(
|
|
1359
|
+
["cargo", "install", "tauri-cli"],
|
|
1360
|
+
check=False,
|
|
1361
|
+
timeout=600,
|
|
1362
|
+
capture_output=False # Show output
|
|
1363
|
+
);
|
|
1364
|
+
if result.returncode == 0 {
|
|
1365
|
+
console.print(" ✔ Tauri CLI installed", style="success");
|
|
1366
|
+
return;
|
|
1367
|
+
} else {
|
|
1368
|
+
console.warning(" Failed to install via cargo. Trying npm...");
|
|
1369
|
+
}
|
|
1370
|
+
} except Exception as e {
|
|
1371
|
+
console.warning(f" Error installing via cargo: {e}");
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
if npm_available {
|
|
1375
|
+
console.print(" Installing Tauri CLI via npm...", style="muted");
|
|
1376
|
+
try {
|
|
1377
|
+
result = subprocess.run(
|
|
1378
|
+
["npm", "install", "-g", "@tauri-apps/cli"],
|
|
1379
|
+
check=False,
|
|
1380
|
+
timeout=300,
|
|
1381
|
+
capture_output=False # Show output
|
|
1382
|
+
);
|
|
1383
|
+
if result.returncode == 0 {
|
|
1384
|
+
console.print(" ✔ Tauri CLI installed", style="success");
|
|
1385
|
+
return;
|
|
1386
|
+
} else {
|
|
1387
|
+
console.warning(" Failed to install via npm");
|
|
1388
|
+
}
|
|
1389
|
+
} except Exception as e {
|
|
1390
|
+
console.warning(f" Error installing via npm: {e}");
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
console.print(" Please install manually:", style="muted");
|
|
1394
|
+
if cargo_available {
|
|
1395
|
+
console.print(" cargo install tauri-cli", style="muted");
|
|
1396
|
+
}
|
|
1397
|
+
if npm_available {
|
|
1398
|
+
console.print(" npm install -g @tauri-apps/cli", style="muted");
|
|
1399
|
+
}
|
|
1400
|
+
} else {
|
|
1401
|
+
console.print(" Skipping Tauri CLI installation.", style="muted");
|
|
1402
|
+
console.print(
|
|
1403
|
+
" It will be needed when building. Install with:", style="muted"
|
|
1404
|
+
);
|
|
1405
|
+
if cargo_available {
|
|
1406
|
+
console.print(" cargo install tauri-cli", style="muted");
|
|
1407
|
+
}
|
|
1408
|
+
if npm_available {
|
|
1409
|
+
console.print(" npm install -g @tauri-apps/cli", style="muted");
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
"""Build desktop app - build web bundle first, then wrap with Tauri."""
|
|
1415
|
+
impl DesktopTarget.build(
|
|
1416
|
+
self: DesktopTarget,
|
|
1417
|
+
entry_file: Path,
|
|
1418
|
+
project_dir: Path,
|
|
1419
|
+
platform: Optional[str] = None
|
|
1420
|
+
) -> Path {
|
|
1421
|
+
import from jac_client.plugin.src.targets.web_target { WebTarget }
|
|
1422
|
+
import from jac_client.plugin.src.vite_bundler { ViteBundler }
|
|
1423
|
+
console.print("\n🖥️ Building desktop app (Tauri)", style="bold");
|
|
1424
|
+
# Check if setup has been run
|
|
1425
|
+
tauri_dir = project_dir / "src-tauri";
|
|
1426
|
+
if not tauri_dir.exists() {
|
|
1427
|
+
raise RuntimeError("Desktop target not set up. Run 'jac setup desktop' first.") ;
|
|
1428
|
+
}
|
|
1429
|
+
# Step 1: Build web bundle first (reuse existing pipeline)
|
|
1430
|
+
console.print(" Step 1: Building web bundle...", style="muted");
|
|
1431
|
+
web_target = WebTarget();
|
|
1432
|
+
web_bundle_path = web_target.build(entry_file, project_dir, platform);
|
|
1433
|
+
console.print(f" ✔ Web bundle built: {web_bundle_path}", style="success");
|
|
1434
|
+
# Step 1.5: Bundle sidecar (optional - can be skipped if not needed)
|
|
1435
|
+
# This bundles the Jac backend as an executable for local use
|
|
1436
|
+
sidecar_bundled = False;
|
|
1437
|
+
try {
|
|
1438
|
+
console.print(" Step 1.5: Bundling sidecar (Jac backend)...", style="muted");
|
|
1439
|
+
sidecar_path = _bundle_sidecar(entry_file, project_dir, tauri_dir, platform);
|
|
1440
|
+
if sidecar_path and sidecar_path.exists() {
|
|
1441
|
+
console.print(f" ✔ Sidecar bundled: {sidecar_path}", style="success");
|
|
1442
|
+
sidecar_bundled = True;
|
|
1443
|
+
}
|
|
1444
|
+
} except Exception as e {
|
|
1445
|
+
console.warning(f" Sidecar bundling skipped: {e}");
|
|
1446
|
+
console.print(
|
|
1447
|
+
" Note: Desktop app will need external API server", style="muted"
|
|
1448
|
+
);
|
|
1449
|
+
sidecar_bundled = False;
|
|
1450
|
+
}
|
|
1451
|
+
# Step 1.5: Check and regenerate icon if needed (AppImage requires square icons)
|
|
1452
|
+
icon_path = tauri_dir / "icons" / "icon.png";
|
|
1453
|
+
if icon_path.exists() {
|
|
1454
|
+
try {
|
|
1455
|
+
# Try to check icon size and squareness using PIL
|
|
1456
|
+
import subprocess;
|
|
1457
|
+
check_code = '''
|
|
1458
|
+
import sys
|
|
1459
|
+
from PIL import Image
|
|
1460
|
+
try:
|
|
1461
|
+
img = Image.open(sys.argv[1])
|
|
1462
|
+
# AppImage requires square icons, and 1024x1024 is recommended
|
|
1463
|
+
if img.width != img.height or img.width < 512:
|
|
1464
|
+
sys.exit(1) # Not square or too small
|
|
1465
|
+
# Verify it's RGBA format
|
|
1466
|
+
if img.mode != "RGBA":
|
|
1467
|
+
sys.exit(1) # Not RGBA
|
|
1468
|
+
sys.exit(0) # Icon OK
|
|
1469
|
+
except:
|
|
1470
|
+
sys.exit(1) # Error or invalid
|
|
1471
|
+
''';
|
|
1472
|
+
result = subprocess.run(
|
|
1473
|
+
["python3", "-c", check_code, str(icon_path)],
|
|
1474
|
+
capture_output=True,
|
|
1475
|
+
timeout=5
|
|
1476
|
+
);
|
|
1477
|
+
if result.returncode != 0 {
|
|
1478
|
+
console.warning(
|
|
1479
|
+
" Icon is invalid or too small for AppImage, regenerating..."
|
|
1480
|
+
);
|
|
1481
|
+
# Delete old icon before regenerating
|
|
1482
|
+
if icon_path.exists() {
|
|
1483
|
+
icon_path.unlink();
|
|
1484
|
+
}
|
|
1485
|
+
_generate_default_icons(tauri_dir);
|
|
1486
|
+
}
|
|
1487
|
+
} except Exception {
|
|
1488
|
+
# If check fails, try to regenerate anyway
|
|
1489
|
+
console.warning(" Could not verify icon, regenerating...");
|
|
1490
|
+
_generate_default_icons(tauri_dir);
|
|
1491
|
+
}
|
|
1492
|
+
} else {
|
|
1493
|
+
# Icon doesn't exist, generate it
|
|
1494
|
+
_generate_default_icons(tauri_dir);
|
|
1495
|
+
}
|
|
1496
|
+
# Step 2: Update tauri.conf.json to point to web bundle
|
|
1497
|
+
console.print(" Step 2: Updating Tauri configuration...", style="muted");
|
|
1498
|
+
_update_tauri_config_for_build(tauri_dir, project_dir, web_bundle_path);
|
|
1499
|
+
# Step 3: Run cargo tauri build
|
|
1500
|
+
console.print(" Step 3: Building Tauri app...", style="muted");
|
|
1501
|
+
bundle_path = _run_tauri_build(tauri_dir, platform);
|
|
1502
|
+
console.success(f"Desktop app built successfully!");
|
|
1503
|
+
console.print(f" Output: {bundle_path}", style="muted");
|
|
1504
|
+
return bundle_path;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
"""Add sidecar binary to tauri.conf.json if it exists."""
|
|
1508
|
+
def _add_sidecar_to_config(tauri_dir: Path, config: dict) -> None {
|
|
1509
|
+
binaries_dir = tauri_dir / "binaries";
|
|
1510
|
+
if not binaries_dir.exists() {
|
|
1511
|
+
return; # No binaries directory, skip
|
|
1512
|
+
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
# Find sidecar wrapper script (must exist and be executable)
|
|
1516
|
+
sidecar_files = [];
|
|
1517
|
+
for pattern in [
|
|
1518
|
+
"jac-sidecar.sh",
|
|
1519
|
+
"jac-sidecar.bat",
|
|
1520
|
+
"jac-sidecar",
|
|
1521
|
+
"jac-sidecar.exe"
|
|
1522
|
+
] {
|
|
1523
|
+
found = list(binaries_dir.glob(pattern));
|
|
1524
|
+
for f in found {
|
|
1525
|
+
if f.is_file() and f.exists() {
|
|
1526
|
+
sidecar_files.append(f);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
if not sidecar_files {
|
|
1532
|
+
# Remove sidecar from resources if it was previously added but no longer exists
|
|
1533
|
+
if "bundle" in config and "resources" in config["bundle"] {
|
|
1534
|
+
resources = config["bundle"]["resources"];
|
|
1535
|
+
# Remove any jac-sidecar entries
|
|
1536
|
+
config["bundle"]["resources"] = [
|
|
1537
|
+
r
|
|
1538
|
+
for r in resources
|
|
1539
|
+
if not ("jac-sidecar" in r or "binaries/jac-sidecar" in r)
|
|
1540
|
+
];
|
|
1541
|
+
}
|
|
1542
|
+
return; # No sidecar found, skip
|
|
1543
|
+
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
# Tauri v2 uses "resources" in bundle section for sidecars
|
|
1547
|
+
if "bundle" not in config {
|
|
1548
|
+
config["bundle"] = {};
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
# Add resources array if it doesn't exist
|
|
1552
|
+
if "resources" not in config["bundle"] {
|
|
1553
|
+
config["bundle"]["resources"] = [];
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
# Add sidecar binary path (relative to tauri_dir)
|
|
1557
|
+
for sidecar_file in sidecar_files {
|
|
1558
|
+
rel_path = sidecar_file.relative_to(tauri_dir);
|
|
1559
|
+
rel_str = str(rel_path.as_posix());
|
|
1560
|
+
if rel_str not in config["bundle"]["resources"] {
|
|
1561
|
+
config["bundle"]["resources"].append(rel_str);
|
|
1562
|
+
console.print(f" ✔ Added sidecar to config: {rel_str}", style="success");
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
"""Populate icon array in tauri.conf.json from icons directory."""
|
|
1568
|
+
def _populate_icon_array(tauri_dir: Path, config: dict) -> None {
|
|
1569
|
+
# Populate icon array if empty or missing
|
|
1570
|
+
if "bundle" not in config {
|
|
1571
|
+
config["bundle"] = {};
|
|
1572
|
+
}
|
|
1573
|
+
if "icon" not in config["bundle"]
|
|
1574
|
+
or not config["bundle"]["icon"]
|
|
1575
|
+
or len(config["bundle"]["icon"]) == 0 {
|
|
1576
|
+
# Scan icons directory for PNG files
|
|
1577
|
+
icons_dir = tauri_dir / "icons";
|
|
1578
|
+
icon_files = [];
|
|
1579
|
+
if icons_dir.exists() {
|
|
1580
|
+
for icon_file in icons_dir.iterdir() {
|
|
1581
|
+
if icon_file.is_file() and icon_file.suffix.lower() == ".png" {
|
|
1582
|
+
# Path relative to tauri.conf.json (which is in tauri_dir)
|
|
1583
|
+
# So icons/icon.png is the correct relative path
|
|
1584
|
+
icon_rel_path = icon_file.relative_to(tauri_dir);
|
|
1585
|
+
icon_files.append(str(icon_rel_path.as_posix()));
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
# Sort to ensure consistent ordering (icon.png first if it exists)
|
|
1590
|
+
icon_files.sort();
|
|
1591
|
+
config["bundle"]["icon"] = icon_files;
|
|
1592
|
+
if icon_files {
|
|
1593
|
+
console.print(
|
|
1594
|
+
f" ✔ Populated icon array with {len(icon_files)} icon(s)",
|
|
1595
|
+
style="success"
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
"""Update tauri.conf.json to point to the built web bundle."""
|
|
1602
|
+
def _update_tauri_config_for_build(
|
|
1603
|
+
tauri_dir: Path,
|
|
1604
|
+
project_dir: Path,
|
|
1605
|
+
web_bundle_path: Path,
|
|
1606
|
+
sidecar_bundled: bool = False
|
|
1607
|
+
) -> None {
|
|
1608
|
+
import json;
|
|
1609
|
+
|
|
1610
|
+
config_path = tauri_dir / "tauri.conf.json";
|
|
1611
|
+
if not config_path.exists() {
|
|
1612
|
+
raise RuntimeError("tauri.conf.json not found. Run 'jac setup desktop' first.") ;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
# Read existing config
|
|
1616
|
+
with open(config_path, "r") as f {
|
|
1617
|
+
config = json.load(f);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
# Calculate relative path from tauri_dir to dist directory (not the bundle file)
|
|
1621
|
+
# web_bundle_path is the JS file, but frontendDist needs the directory containing index.html
|
|
1622
|
+
dist_dir = web_bundle_path.parent;
|
|
1623
|
+
|
|
1624
|
+
try {
|
|
1625
|
+
# Dist directory is typically in .jac/client/dist
|
|
1626
|
+
# Relative to src-tauri, that's ../.jac/client/dist
|
|
1627
|
+
dist_relative = dist_dir.relative_to(project_dir);
|
|
1628
|
+
dist_relative_str = "../" + str(dist_relative.as_posix());
|
|
1629
|
+
} except ValueError {
|
|
1630
|
+
# If paths don't share a common root, use absolute path
|
|
1631
|
+
dist_relative_str = str(dist_dir.as_posix());
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
# Update build config (Tauri v2 structure)
|
|
1635
|
+
if "build" not in config {
|
|
1636
|
+
config["build"] = {};
|
|
1637
|
+
}
|
|
1638
|
+
# Tauri v2 uses 'frontendDist' to point to the directory containing index.html
|
|
1639
|
+
config["build"]["frontendDist"] = dist_relative_str;
|
|
1640
|
+
# Remove devUrl and other invalid properties
|
|
1641
|
+
if "devUrl" in config["build"] {
|
|
1642
|
+
del config["build"]["devUrl"];
|
|
1643
|
+
}
|
|
1644
|
+
if "devPath" in config["build"] {
|
|
1645
|
+
del config["build"]["devPath"];
|
|
1646
|
+
}
|
|
1647
|
+
if "distDir" in config["build"] {
|
|
1648
|
+
del config["build"]["distDir"];
|
|
1649
|
+
}
|
|
1650
|
+
if "withGlobalTauri" in config["build"] {
|
|
1651
|
+
del config["build"]["withGlobalTauri"];
|
|
1652
|
+
}
|
|
1653
|
+
# Clean up null values
|
|
1654
|
+
if "beforeBuildCommand" in config["build"]
|
|
1655
|
+
and config["build"]["beforeBuildCommand"] is None {
|
|
1656
|
+
del config["build"]["beforeBuildCommand"];
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
# Populate icon array if empty or missing
|
|
1660
|
+
_populate_icon_array(tauri_dir, config);
|
|
1661
|
+
|
|
1662
|
+
# Add sidecar binary if it exists
|
|
1663
|
+
_add_sidecar_to_config(tauri_dir, config);
|
|
1664
|
+
|
|
1665
|
+
# Write updated config
|
|
1666
|
+
with open(config_path, "w") as f {
|
|
1667
|
+
json.dump(config, f, indent=2);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
console.print(" ✔ Updated tauri.conf.json", style="success");
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
"""Check if appimagetool is available and can run."""
|
|
1674
|
+
def _check_appimagetool -> tuple[bool, str | None] {
|
|
1675
|
+
# First check if the command exists
|
|
1676
|
+
try {
|
|
1677
|
+
result = subprocess.run(
|
|
1678
|
+
["which", "appimagetool"], capture_output=True, timeout=2
|
|
1679
|
+
);
|
|
1680
|
+
if result.returncode != 0 {
|
|
1681
|
+
return (False, "appimagetool not found in PATH");
|
|
1682
|
+
}
|
|
1683
|
+
} except Exception {
|
|
1684
|
+
return (False, "Could not check for appimagetool");
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
# Try to run appimagetool to see if it works
|
|
1688
|
+
try {
|
|
1689
|
+
result = subprocess.run(
|
|
1690
|
+
["appimagetool", "--version"], capture_output=True, timeout=5, text=True
|
|
1691
|
+
);
|
|
1692
|
+
if result.returncode == 0 {
|
|
1693
|
+
return (True, None);
|
|
1694
|
+
} else {
|
|
1695
|
+
# Check if it's a FUSE error
|
|
1696
|
+
error_output = result.stderr or result.stdout or "";
|
|
1697
|
+
if "fuse" in error_output.lower() or "libfuse" in error_output.lower() {
|
|
1698
|
+
return (
|
|
1699
|
+
False,
|
|
1700
|
+
"FUSE library not available. Install with: sudo apt-get install libfuse2"
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
return (False, f"appimagetool failed: {error_output[:100]}");
|
|
1704
|
+
}
|
|
1705
|
+
} except FileNotFoundError {
|
|
1706
|
+
return (False, "appimagetool not found");
|
|
1707
|
+
} except subprocess.TimeoutExpired {
|
|
1708
|
+
return (False, "appimagetool check timed out");
|
|
1709
|
+
} except Exception as e {
|
|
1710
|
+
return (False, f"Error checking appimagetool: {str(e)[:100]}");
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
"""Create AppImage from AppDir if appimagetool is available."""
|
|
1715
|
+
def _create_appimage_from_appdir(appdir_path: Path) -> Path | None {
|
|
1716
|
+
if not appdir_path.exists() or not appdir_path.is_dir() {
|
|
1717
|
+
return None;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
# Check if appimagetool is available and working
|
|
1721
|
+
(is_available, error_msg) = _check_appimagetool();
|
|
1722
|
+
if not is_available {
|
|
1723
|
+
console.warning(" appimagetool not available. AppImage will not be created.");
|
|
1724
|
+
if error_msg {
|
|
1725
|
+
console.print(f" {error_msg}", style="muted");
|
|
1726
|
+
}
|
|
1727
|
+
# Provide installation instructions based on the error
|
|
1728
|
+
if error_msg and "fuse" in error_msg.lower() {
|
|
1729
|
+
console.print(" For WSL2, you may need to install FUSE:", style="muted");
|
|
1730
|
+
console.print(
|
|
1731
|
+
" sudo apt-get update && sudo apt-get install -y libfuse2",
|
|
1732
|
+
style="muted"
|
|
1733
|
+
);
|
|
1734
|
+
console.print(
|
|
1735
|
+
" Or use --appimage-extract to extract appimagetool first",
|
|
1736
|
+
style="muted"
|
|
1737
|
+
);
|
|
1738
|
+
} else {
|
|
1739
|
+
console.print(" Install it with:", style="muted");
|
|
1740
|
+
console.print(
|
|
1741
|
+
" wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage",
|
|
1742
|
+
style="muted"
|
|
1743
|
+
);
|
|
1744
|
+
console.print(" chmod +x appimagetool-x86_64.AppImage", style="muted");
|
|
1745
|
+
console.print(
|
|
1746
|
+
" sudo mv appimagetool-x86_64.AppImage /usr/local/bin/appimagetool",
|
|
1747
|
+
style="muted"
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1750
|
+
return None;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
# Determine AppImage filename from AppDir name
|
|
1754
|
+
appimage_name = f"{appdir_path.name}.AppImage";
|
|
1755
|
+
appimage_path = appdir_path.parent / appimage_name;
|
|
1756
|
+
|
|
1757
|
+
# Skip if AppImage already exists and is newer than AppDir
|
|
1758
|
+
if appimage_path.exists() {
|
|
1759
|
+
appdir_mtime = appdir_path.stat().st_mtime;
|
|
1760
|
+
appimage_mtime = appimage_path.stat().st_mtime;
|
|
1761
|
+
if appimage_mtime >= appdir_mtime {
|
|
1762
|
+
console.print(
|
|
1763
|
+
f" ✔ AppImage already exists: {appimage_path.name}", style="success"
|
|
1764
|
+
);
|
|
1765
|
+
return appimage_path;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
try {
|
|
1770
|
+
console.print(f" Creating AppImage from {appdir_path.name}...", style="muted");
|
|
1771
|
+
result = subprocess.run(
|
|
1772
|
+
["appimagetool", str(appdir_path), str(appimage_path)],
|
|
1773
|
+
cwd=appdir_path.parent,
|
|
1774
|
+
check=False, # Don't raise on error, we'll check manually
|
|
1775
|
+
capture_output=True,
|
|
1776
|
+
text=True,
|
|
1777
|
+
timeout=300 # 5 minute timeout
|
|
1778
|
+
);
|
|
1779
|
+
|
|
1780
|
+
if result.returncode != 0 {
|
|
1781
|
+
error_output = result.stderr or result.stdout or "Unknown error";
|
|
1782
|
+
# Check for FUSE-related errors
|
|
1783
|
+
if "fuse" in error_output.lower()
|
|
1784
|
+
or "libfuse" in error_output.lower()
|
|
1785
|
+
or "FUSE" in error_output {
|
|
1786
|
+
console.warning(
|
|
1787
|
+
" Failed to create AppImage: FUSE library not available"
|
|
1788
|
+
);
|
|
1789
|
+
console.print(
|
|
1790
|
+
" Install FUSE with: sudo apt-get install -y libfuse2",
|
|
1791
|
+
style="muted"
|
|
1792
|
+
);
|
|
1793
|
+
console.print(
|
|
1794
|
+
" For WSL2, you may also need: sudo apt-get install -y fuse",
|
|
1795
|
+
style="muted"
|
|
1796
|
+
);
|
|
1797
|
+
} else {
|
|
1798
|
+
console.warning(f" Failed to create AppImage: {error_output[:200]}");
|
|
1799
|
+
}
|
|
1800
|
+
return None;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
# Verify the AppImage was created
|
|
1804
|
+
if not appimage_path.exists() {
|
|
1805
|
+
console.warning(" AppImage creation reported success but file not found");
|
|
1806
|
+
return None;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
# Make AppImage executable
|
|
1810
|
+
appimage_path.chmod(appimage_path.stat().st_mode | stat.S_IEXEC);
|
|
1811
|
+
|
|
1812
|
+
console.print(f" ✔ AppImage created: {appimage_path.name}", style="success");
|
|
1813
|
+
return appimage_path;
|
|
1814
|
+
} except subprocess.TimeoutExpired {
|
|
1815
|
+
console.warning(" AppImage creation timed out");
|
|
1816
|
+
return None;
|
|
1817
|
+
} except Exception as e {
|
|
1818
|
+
error_msg = str(e);
|
|
1819
|
+
if "fuse" in error_msg.lower() or "libfuse" in error_msg.lower() {
|
|
1820
|
+
console.warning(
|
|
1821
|
+
" FUSE library error. Install with: sudo apt-get install -y libfuse2"
|
|
1822
|
+
);
|
|
1823
|
+
} else {
|
|
1824
|
+
console.warning(f" Error creating AppImage: {error_msg[:200]}");
|
|
1825
|
+
}
|
|
1826
|
+
return None;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
"""Run cargo tauri build and return path to bundle."""
|
|
1831
|
+
def _run_tauri_build(tauri_dir: Path, platform: Optional[str] = None) -> Path {
|
|
1832
|
+
import os;
|
|
1833
|
+
|
|
1834
|
+
# Determine target based on platform
|
|
1835
|
+
target = None;
|
|
1836
|
+
if platform == "windows" {
|
|
1837
|
+
target = "x86_64-pc-windows-msvc";
|
|
1838
|
+
} elif platform == "macos" {
|
|
1839
|
+
# Try to detect architecture
|
|
1840
|
+
if platform.machine() == "arm64" {
|
|
1841
|
+
target = "aarch64-apple-darwin";
|
|
1842
|
+
} else {
|
|
1843
|
+
target = "x86_64-apple-darwin";
|
|
1844
|
+
}
|
|
1845
|
+
} elif platform == "linux" {
|
|
1846
|
+
target = "x86_64-unknown-linux-gnu";
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
# Build command
|
|
1850
|
+
build_cmd = ["npm", "run", "tauri", "build"];
|
|
1851
|
+
if target {
|
|
1852
|
+
build_cmd.extend(["--", "--client", target]);
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
# Check if package.json has tauri scripts, if not, use cargo directly
|
|
1856
|
+
package_json = tauri_dir.parent / "package.json";
|
|
1857
|
+
use_npm = False;
|
|
1858
|
+
if package_json.exists() {
|
|
1859
|
+
try {
|
|
1860
|
+
with open(package_json, "r") as f {
|
|
1861
|
+
package_data = json.load(f);
|
|
1862
|
+
scripts = package_data.get("scripts", {});
|
|
1863
|
+
if "tauri" in scripts or "tauri:build" in scripts {
|
|
1864
|
+
use_npm = True;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
} except Exception {
|
|
1868
|
+
console.warning(" Failed to check package.json for tauri scripts");
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
if not use_npm {
|
|
1873
|
+
# Use cargo tauri build directly
|
|
1874
|
+
build_cmd = ["cargo", "tauri", "build"];
|
|
1875
|
+
if target {
|
|
1876
|
+
build_cmd.extend(["--client", target]);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
# Change to tauri directory and run build
|
|
1881
|
+
original_cwd = os.getcwd();
|
|
1882
|
+
try {
|
|
1883
|
+
os.chdir(tauri_dir.parent);
|
|
1884
|
+
console.print(f" Running: {' '.join(build_cmd)}", style="muted");
|
|
1885
|
+
|
|
1886
|
+
result = subprocess.run(
|
|
1887
|
+
build_cmd,
|
|
1888
|
+
cwd=tauri_dir.parent,
|
|
1889
|
+
check=True,
|
|
1890
|
+
capture_output=False # Show output to user
|
|
1891
|
+
);
|
|
1892
|
+
} finally {
|
|
1893
|
+
os.chdir(original_cwd);
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
# Find the bundle output
|
|
1897
|
+
# Tauri outputs to src-tauri/target/{target}/release/bundle/
|
|
1898
|
+
if target {
|
|
1899
|
+
bundle_dir = tauri_dir / "target" / target / "release" / "bundle";
|
|
1900
|
+
} else {
|
|
1901
|
+
bundle_dir = tauri_dir / "target" / "release" / "bundle";
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
if not bundle_dir.exists() {
|
|
1905
|
+
raise RuntimeError(f"Build completed but bundle not found at: {bundle_dir}") ;
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
# Find the actual bundle file (varies by platform)
|
|
1909
|
+
bundle_files = list(bundle_dir.rglob("*"));
|
|
1910
|
+
installers = [
|
|
1911
|
+
f
|
|
1912
|
+
for f in bundle_files
|
|
1913
|
+
if f.is_file()
|
|
1914
|
+
and f.suffix in [".exe", ".dmg", ".AppImage", ".deb", ".rpm", ".msi"]
|
|
1915
|
+
];
|
|
1916
|
+
|
|
1917
|
+
# For Linux: if no AppImage but AppDir exists, try to create AppImage
|
|
1918
|
+
is_linux_build = (platform == "linux")
|
|
1919
|
+
or (not platform and target and "linux" in target);
|
|
1920
|
+
if not installers and is_linux_build {
|
|
1921
|
+
appimage_dir = bundle_dir / "appimage";
|
|
1922
|
+
if appimage_dir.exists() {
|
|
1923
|
+
# Look for AppDir directories
|
|
1924
|
+
appdirs = [
|
|
1925
|
+
d
|
|
1926
|
+
for d in appimage_dir.iterdir()
|
|
1927
|
+
if d.is_dir() and d.name.endswith(".AppDir")
|
|
1928
|
+
];
|
|
1929
|
+
if appdirs {
|
|
1930
|
+
# Try to create AppImage from the first AppDir found
|
|
1931
|
+
appimage_path = _create_appimage_from_appdir(appdirs[0]);
|
|
1932
|
+
if appimage_path and appimage_path.exists() {
|
|
1933
|
+
installers = [appimage_path];
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
if installers {
|
|
1940
|
+
return installers[0];
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
# Fallback: return the bundle directory
|
|
1944
|
+
return bundle_dir;
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
"""Create Python-based sidecar wrapper script (no PyInstaller needed)."""
|
|
1948
|
+
def _bundle_sidecar(
|
|
1949
|
+
entry_file: Path,
|
|
1950
|
+
project_dir: Path,
|
|
1951
|
+
tauri_dir: Path,
|
|
1952
|
+
platform: Optional[str] = None
|
|
1953
|
+
) -> Path | None {
|
|
1954
|
+
import platform as platform_module;
|
|
1955
|
+
import stat;
|
|
1956
|
+
|
|
1957
|
+
# Determine output directory
|
|
1958
|
+
binaries_dir = tauri_dir / "binaries";
|
|
1959
|
+
binaries_dir.mkdir(parents=True, exist_ok=True);
|
|
1960
|
+
|
|
1961
|
+
# Determine script name based on platform
|
|
1962
|
+
if platform == "windows"
|
|
1963
|
+
or (platform is None and platform_module.system() == "Windows") {
|
|
1964
|
+
script_name = "jac-sidecar.bat";
|
|
1965
|
+
is_windows = True;
|
|
1966
|
+
} else {
|
|
1967
|
+
script_name = "jac-sidecar.sh";
|
|
1968
|
+
is_windows = False;
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
output_path = binaries_dir / script_name;
|
|
1972
|
+
|
|
1973
|
+
# Create wrapper script that runs Python module
|
|
1974
|
+
# This approach avoids PyInstaller issues and is much simpler
|
|
1975
|
+
if is_windows {
|
|
1976
|
+
# Windows batch script
|
|
1977
|
+
script_content = f'''@echo off
|
|
1978
|
+
REM Jac Sidecar Wrapper - Runs Jac backend using system Python
|
|
1979
|
+
REM This requires Python and jaclang to be installed
|
|
1980
|
+
|
|
1981
|
+
python -m jac_client.plugin.src.targets.desktop.sidecar.main %*
|
|
1982
|
+
''';
|
|
1983
|
+
} else {
|
|
1984
|
+
# Unix shell script
|
|
1985
|
+
script_content = '''#!/bin/bash
|
|
1986
|
+
# Jac Sidecar Wrapper - Runs Jac backend using system Python
|
|
1987
|
+
# This requires Python and jaclang to be installed
|
|
1988
|
+
|
|
1989
|
+
exec python -m jac_client.plugin.src.targets.desktop.sidecar.main "$@"
|
|
1990
|
+
''';
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
# Write the wrapper script
|
|
1994
|
+
with open(output_path, "w") as f {
|
|
1995
|
+
f.write(script_content);
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
# Make executable on Unix systems
|
|
1999
|
+
if not is_windows {
|
|
2000
|
+
st = output_path.stat();
|
|
2001
|
+
output_path.chmod(st.st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH);
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
console.print(f" ✔ Created sidecar wrapper: {script_name}", style="success");
|
|
2005
|
+
console.print(
|
|
2006
|
+
f" Note: Requires Python and jaclang to be installed", style="muted"
|
|
2007
|
+
);
|
|
2008
|
+
console.print(f" Run: pip install jaclang", style="muted");
|
|
2009
|
+
|
|
2010
|
+
return output_path;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
"""Start desktop dev server - start web dev server and launch tauri dev."""
|
|
2014
|
+
impl DesktopTarget.dev(
|
|
2015
|
+
self: DesktopTarget, entry_file: Path, project_dir: Path
|
|
2016
|
+
) -> None {
|
|
2017
|
+
import from jac_client.plugin.src.vite_bundler { ViteBundler }
|
|
2018
|
+
import signal;
|
|
2019
|
+
import sys;
|
|
2020
|
+
console.print("\n🖥️ Starting desktop dev server (Tauri)", style="bold");
|
|
2021
|
+
# Check if setup has been run
|
|
2022
|
+
tauri_dir = project_dir / "src-tauri";
|
|
2023
|
+
if not tauri_dir.exists() {
|
|
2024
|
+
raise RuntimeError("Desktop target not set up. Run 'jac setup desktop' first.") ;
|
|
2025
|
+
}
|
|
2026
|
+
# Step 1: Update tauri.conf.json for dev mode
|
|
2027
|
+
console.print(" Configuring Tauri for dev mode...", style="muted");
|
|
2028
|
+
_update_tauri_config_for_dev(tauri_dir);
|
|
2029
|
+
# Step 1.5: Prepare package.json and get compiler
|
|
2030
|
+
# We need compiled/_entry.js and compiled/main.js to exist before Vite can serve them
|
|
2031
|
+
console.print(" Preparing compilation setup...", style="muted");
|
|
2032
|
+
vite_port = 5173;
|
|
2033
|
+
bundler = ViteBundler(
|
|
2034
|
+
project_dir=project_dir, output_dir=None, minify=False, config_path=None
|
|
2035
|
+
);
|
|
2036
|
+
# Ensure package.json exists (needed for ViteCompiler)
|
|
2037
|
+
package_json_path = bundler._get_client_dir() / 'configs' / 'package.json';
|
|
2038
|
+
if not package_json_path.exists() {
|
|
2039
|
+
bundler.create_package_json();
|
|
2040
|
+
}
|
|
2041
|
+
# Get the compiler via JacClient (this provides all the compile functions)
|
|
2042
|
+
import from jac_client.plugin.client { JacClient }
|
|
2043
|
+
builder = JacClient.get_client_bundle_builder();
|
|
2044
|
+
# Set the package.json path if not already set
|
|
2045
|
+
if not builder.vite_package_json or not builder.vite_package_json.exists() {
|
|
2046
|
+
builder.vite_package_json = package_json_path;
|
|
2047
|
+
}
|
|
2048
|
+
compiler = builder._get_compiler();
|
|
2049
|
+
# Ensure compiled directory exists
|
|
2050
|
+
compiler.compiled_dir.mkdir(parents=True, exist_ok=True);
|
|
2051
|
+
# Compile runtime utils (creates client_runtime.js)
|
|
2052
|
+
compiler.compile_runtime_utils();
|
|
2053
|
+
# Compile the module to create compiled/main.js (or whatever the module name is)
|
|
2054
|
+
(module_js, mod, module_manifest) = compiler.jac_compiler.compile_module(
|
|
2055
|
+
entry_file
|
|
2056
|
+
);
|
|
2057
|
+
# Write the compiled module to compiled directory
|
|
2058
|
+
module_name = entry_file.stem;
|
|
2059
|
+
compiled_module_path = compiler.compiled_dir / f"{module_name}.js";
|
|
2060
|
+
compiled_module_path.write_text(module_js, encoding='utf-8');
|
|
2061
|
+
# Create the entry file (this will be used by Vite dev server)
|
|
2062
|
+
compiler.create_entry_file(entry_file);
|
|
2063
|
+
console.print(" ✔ Module compiled for dev mode", style="success");
|
|
2064
|
+
# Step 2: Start web dev server
|
|
2065
|
+
console.print(" Starting web dev server...", style="muted");
|
|
2066
|
+
# Create dev vite config
|
|
2067
|
+
dev_config_path = bundler.create_dev_vite_config(entry_file, api_port=8000);
|
|
2068
|
+
# Start Vite dev server
|
|
2069
|
+
vite_process = bundler.start_dev_server(port=vite_port);
|
|
2070
|
+
if not vite_process {
|
|
2071
|
+
raise RuntimeError("Failed to start Vite dev server") ;
|
|
2072
|
+
}
|
|
2073
|
+
console.print(
|
|
2074
|
+
f" ✔ Web dev server running on http://localhost:{vite_port}", style="success"
|
|
2075
|
+
);
|
|
2076
|
+
# Step 3: Launch tauri dev
|
|
2077
|
+
console.print(" Launching Tauri dev window...", style="muted");
|
|
2078
|
+
console.print(" (Press Ctrl+C to stop)", style="muted");
|
|
2079
|
+
# Setup signal handlers for cleanup
|
|
2080
|
+
def cleanup -> None {
|
|
2081
|
+
console.print("\n Stopping dev servers...", style="muted");
|
|
2082
|
+
if vite_process {
|
|
2083
|
+
try {
|
|
2084
|
+
vite_process.terminate();
|
|
2085
|
+
vite_process.wait(timeout=5);
|
|
2086
|
+
} except Exception {
|
|
2087
|
+
vite_process.kill();
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
console.print(" ✔ Dev servers stopped", style="success");
|
|
2091
|
+
}
|
|
2092
|
+
def signal_handler(signum: int, frame: Any) -> None {
|
|
2093
|
+
cleanup();
|
|
2094
|
+
sys.exit(0);
|
|
2095
|
+
}
|
|
2096
|
+
signal.signal(signal.SIGINT, signal_handler);
|
|
2097
|
+
signal.signal(signal.SIGTERM, signal_handler);
|
|
2098
|
+
try {
|
|
2099
|
+
# Run tauri dev
|
|
2100
|
+
tauri_process = _run_tauri_dev(tauri_dir);
|
|
2101
|
+
|
|
2102
|
+
# Wait for tauri process to finish
|
|
2103
|
+
tauri_process.wait();
|
|
2104
|
+
} except KeyboardInterrupt {
|
|
2105
|
+
console.print(
|
|
2106
|
+
"\n Keyboard interrupt detected. Stopping dev servers...", style="muted"
|
|
2107
|
+
);
|
|
2108
|
+
} finally {
|
|
2109
|
+
cleanup();
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
"""Update tauri.conf.json for dev mode (point to dev server)."""
|
|
2114
|
+
def _update_tauri_config_for_dev(tauri_dir: Path) -> None {
|
|
2115
|
+
import json;
|
|
2116
|
+
|
|
2117
|
+
config_path = tauri_dir / "tauri.conf.json";
|
|
2118
|
+
if not config_path.exists() {
|
|
2119
|
+
raise RuntimeError("tauri.conf.json not found. Run 'jac setup desktop' first.") ;
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
# Read existing config
|
|
2123
|
+
with open(config_path, "r") as f {
|
|
2124
|
+
config = json.load(f);
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
# Update build config for dev mode (Tauri v2 structure)
|
|
2128
|
+
if "build" not in config {
|
|
2129
|
+
config["build"] = {};
|
|
2130
|
+
}
|
|
2131
|
+
# Tauri v2 uses 'devUrl' instead of 'devPath'
|
|
2132
|
+
config["build"]["devUrl"] = "http://localhost:5173";
|
|
2133
|
+
# Remove distDir and other invalid properties
|
|
2134
|
+
if "distDir" in config["build"] {
|
|
2135
|
+
del config["build"]["distDir"];
|
|
2136
|
+
}
|
|
2137
|
+
if "devPath" in config["build"] {
|
|
2138
|
+
del config["build"]["devPath"];
|
|
2139
|
+
}
|
|
2140
|
+
if "withGlobalTauri" in config["build"] {
|
|
2141
|
+
del config["build"]["withGlobalTauri"];
|
|
2142
|
+
}
|
|
2143
|
+
# Keep only valid properties
|
|
2144
|
+
if "beforeDevCommand" in config["build"]
|
|
2145
|
+
and config["build"]["beforeDevCommand"] is None {
|
|
2146
|
+
del config["build"]["beforeDevCommand"];
|
|
2147
|
+
}
|
|
2148
|
+
if "beforeBuildCommand" in config["build"]
|
|
2149
|
+
and config["build"]["beforeBuildCommand"] is None {
|
|
2150
|
+
del config["build"]["beforeBuildCommand"];
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
# Populate icon array if empty or missing
|
|
2154
|
+
_populate_icon_array(tauri_dir, config);
|
|
2155
|
+
|
|
2156
|
+
# Add sidecar binary if it exists
|
|
2157
|
+
_add_sidecar_to_config(tauri_dir, config);
|
|
2158
|
+
|
|
2159
|
+
# Write updated config
|
|
2160
|
+
with open(config_path, "w") as f {
|
|
2161
|
+
json.dump(config, f, indent=2);
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
console.print(" ✔ Updated tauri.conf.json for dev mode", style="success");
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
"""Run tauri dev command."""
|
|
2168
|
+
def _run_tauri_dev(tauri_dir: Path) -> subprocess.Popen {
|
|
2169
|
+
# Check if cargo is available
|
|
2170
|
+
try {
|
|
2171
|
+
subprocess.run(
|
|
2172
|
+
["cargo", "--version"], capture_output=True, check=True, timeout=5
|
|
2173
|
+
);
|
|
2174
|
+
} except (
|
|
2175
|
+
subprocess.CalledProcessError,
|
|
2176
|
+
FileNotFoundError,
|
|
2177
|
+
subprocess.TimeoutExpired
|
|
2178
|
+
) {
|
|
2179
|
+
raise RuntimeError(
|
|
2180
|
+
"Rust/Cargo not found. Install Rust from https://rustup.rs/\n"
|
|
2181
|
+
"Or run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
|
|
2182
|
+
) ;
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
# Check if build tools (gcc/cc) are available
|
|
2186
|
+
try {
|
|
2187
|
+
subprocess.run(["cc", "--version"], capture_output=True, check=True, timeout=5);
|
|
2188
|
+
} except (
|
|
2189
|
+
subprocess.CalledProcessError,
|
|
2190
|
+
FileNotFoundError,
|
|
2191
|
+
subprocess.TimeoutExpired
|
|
2192
|
+
) {
|
|
2193
|
+
raise RuntimeError(
|
|
2194
|
+
"Build tools not found. Install build-essential:\n"
|
|
2195
|
+
" Ubuntu/Debian: sudo apt-get install build-essential\n"
|
|
2196
|
+
" Fedora: sudo dnf install gcc gcc-c++\n"
|
|
2197
|
+
" Arch: sudo pacman -S base-devel"
|
|
2198
|
+
) ;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
# Check if tauri CLI is available via cargo
|
|
2202
|
+
try {
|
|
2203
|
+
subprocess.run(
|
|
2204
|
+
["cargo", "tauri", "--version"], capture_output=True, check=True, timeout=5
|
|
2205
|
+
);
|
|
2206
|
+
} except (
|
|
2207
|
+
subprocess.CalledProcessError,
|
|
2208
|
+
FileNotFoundError,
|
|
2209
|
+
subprocess.TimeoutExpired
|
|
2210
|
+
) {
|
|
2211
|
+
console.warning("Tauri CLI not found.");
|
|
2212
|
+
console.print(" Install manually: cargo install tauri-cli", style="muted");
|
|
2213
|
+
console.print(" Or use npm: npm install -D @tauri-apps/cli", style="muted");
|
|
2214
|
+
raise RuntimeError(
|
|
2215
|
+
"Tauri CLI not installed. Install it first:\n"
|
|
2216
|
+
" cargo install tauri-cli\n"
|
|
2217
|
+
" Or: npm install -D @tauri-apps/cli"
|
|
2218
|
+
) ;
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
# Check if package.json has tauri scripts
|
|
2222
|
+
package_json = tauri_dir.parent / "package.json";
|
|
2223
|
+
use_npm = False;
|
|
2224
|
+
if package_json.exists() {
|
|
2225
|
+
try {
|
|
2226
|
+
with open(package_json, "r") as f {
|
|
2227
|
+
package_data = json.load(f);
|
|
2228
|
+
scripts = package_data.get("scripts", {});
|
|
2229
|
+
if "tauri" in scripts or "tauri:dev" in scripts {
|
|
2230
|
+
use_npm = True;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
} except Exception { }
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
if use_npm {
|
|
2237
|
+
# Use npm run tauri dev
|
|
2238
|
+
dev_cmd = ["npm", "run", "tauri", "dev"];
|
|
2239
|
+
} else {
|
|
2240
|
+
# Use cargo tauri dev directly
|
|
2241
|
+
dev_cmd = ["cargo", "tauri", "dev"];
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
console.print(f" Running: {' '.join(dev_cmd)}", style="muted");
|
|
2245
|
+
|
|
2246
|
+
# Run tauri dev (this will open a window)
|
|
2247
|
+
# Use None for stdout/stderr to show output directly in terminal
|
|
2248
|
+
try {
|
|
2249
|
+
process = subprocess.Popen(
|
|
2250
|
+
dev_cmd,
|
|
2251
|
+
cwd=tauri_dir.parent,
|
|
2252
|
+
stdout=None, # Show output directly
|
|
2253
|
+
stderr=None, # Show errors directly
|
|
2254
|
+
text=True
|
|
2255
|
+
);
|
|
2256
|
+
return process;
|
|
2257
|
+
} except Exception as e {
|
|
2258
|
+
console.error(f" Failed to start Tauri: {e}");
|
|
2259
|
+
raise ;
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
"""Start desktop app - build web bundle and launch Tauri with built bundle."""
|
|
2264
|
+
impl DesktopTarget.start(
|
|
2265
|
+
self: DesktopTarget, entry_file: Path, project_dir: Path
|
|
2266
|
+
) -> None {
|
|
2267
|
+
import from jac_client.plugin.src.targets.web_target { WebTarget }
|
|
2268
|
+
import signal;
|
|
2269
|
+
import sys;
|
|
2270
|
+
console.print("\n🖥️ Starting desktop app (Tauri)", style="bold");
|
|
2271
|
+
# Check if setup has been run
|
|
2272
|
+
tauri_dir = project_dir / "src-tauri";
|
|
2273
|
+
if not tauri_dir.exists() {
|
|
2274
|
+
raise RuntimeError("Desktop target not set up. Run 'jac setup desktop' first.") ;
|
|
2275
|
+
}
|
|
2276
|
+
# Step 1: Build web bundle first
|
|
2277
|
+
console.print(" Step 1: Building web bundle...", style="muted");
|
|
2278
|
+
web_target = WebTarget();
|
|
2279
|
+
web_bundle_path = web_target.build(entry_file, project_dir, None);
|
|
2280
|
+
console.print(f" ✔ Web bundle built: {web_bundle_path}", style="success");
|
|
2281
|
+
# Step 2: Update tauri.conf.json to point to web bundle
|
|
2282
|
+
console.print(" Step 2: Updating Tauri configuration...", style="muted");
|
|
2283
|
+
_update_tauri_config_for_build(tauri_dir, project_dir, web_bundle_path);
|
|
2284
|
+
# Step 3: Launch tauri dev (which will use the built bundle)
|
|
2285
|
+
console.print(" Step 3: Launching Tauri app...", style="muted");
|
|
2286
|
+
console.print(" (Press Ctrl+C to stop)", style="muted");
|
|
2287
|
+
# Setup signal handlers for cleanup
|
|
2288
|
+
def cleanup -> None {
|
|
2289
|
+
console.print("\n Stopping app...", style="muted");
|
|
2290
|
+
console.print(" ✔ App stopped", style="success");
|
|
2291
|
+
}
|
|
2292
|
+
def signal_handler(signum: int, frame: Any) -> None {
|
|
2293
|
+
cleanup();
|
|
2294
|
+
sys.exit(0);
|
|
2295
|
+
}
|
|
2296
|
+
signal.signal(signal.SIGINT, signal_handler);
|
|
2297
|
+
signal.signal(signal.SIGTERM, signal_handler);
|
|
2298
|
+
tauri_process = None;
|
|
2299
|
+
try {
|
|
2300
|
+
# Run tauri dev (it will use the built bundle from distDir)
|
|
2301
|
+
tauri_process = _run_tauri_dev(tauri_dir);
|
|
2302
|
+
|
|
2303
|
+
if not tauri_process {
|
|
2304
|
+
console.error(" Failed to start Tauri process");
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
# Wait for tauri process to finish
|
|
2309
|
+
return_code = tauri_process.wait();
|
|
2310
|
+
if return_code != 0 {
|
|
2311
|
+
console.warning(f" Tauri process exited with code {return_code}");
|
|
2312
|
+
}
|
|
2313
|
+
} except KeyboardInterrupt {
|
|
2314
|
+
console.print(
|
|
2315
|
+
"\n Keyboard interrupt detected. Stopping app...", style="muted"
|
|
2316
|
+
);
|
|
2317
|
+
if tauri_process {
|
|
2318
|
+
try {
|
|
2319
|
+
tauri_process.terminate();
|
|
2320
|
+
tauri_process.wait(timeout=5);
|
|
2321
|
+
} except Exception {
|
|
2322
|
+
if tauri_process {
|
|
2323
|
+
tauri_process.kill();
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
} except Exception as e {
|
|
2328
|
+
console.error(f" Error starting desktop app: {e}");
|
|
2329
|
+
import traceback;
|
|
2330
|
+
console.print(traceback.format_exc(), style="muted");
|
|
2331
|
+
} finally {
|
|
2332
|
+
cleanup();
|
|
2333
|
+
}
|
|
2334
|
+
}
|