electron-cli 0.3.0-alpha.2 → 0.3.0-alpha.4
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.
- package/Cargo.lock +96 -1
- package/Cargo.toml +2 -1
- package/README.md +28 -9
- package/package.json +2 -1
- package/src/cli.rs +120 -3
- package/src/commands/init.rs +413 -27
- package/src/commands/make.rs +426 -0
- package/src/commands/mod.rs +3 -0
- package/src/commands/package.rs +703 -0
- package/src/commands/plan.rs +50 -5
- package/src/commands/start.rs +287 -0
- package/src/main.rs +3 -0
- package/templates/minimal/gitignore +5 -0
- package/templates/minimal/src/index.html +82 -0
- package/templates/minimal/src/main.js +33 -0
- package/templates/minimal/src/preload.js +6 -0
- package/templates/minimal/src/renderer.js +5 -0
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
use std::{
|
|
2
|
+
fs,
|
|
3
|
+
path::{Path, PathBuf},
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
use anyhow::{bail, Context, Result};
|
|
7
|
+
use camino::Utf8PathBuf;
|
|
8
|
+
use serde::Serialize;
|
|
9
|
+
|
|
10
|
+
use crate::{cli::PackageArgs, output, project::ProjectSnapshot};
|
|
11
|
+
|
|
12
|
+
#[derive(Debug, Serialize)]
|
|
13
|
+
pub(crate) struct PackageReport {
|
|
14
|
+
project: ProjectSnapshot,
|
|
15
|
+
app_name: String,
|
|
16
|
+
executable_name: String,
|
|
17
|
+
platform: String,
|
|
18
|
+
arch: String,
|
|
19
|
+
electron_dist: Utf8PathBuf,
|
|
20
|
+
output_dir: Utf8PathBuf,
|
|
21
|
+
bundle_dir: Utf8PathBuf,
|
|
22
|
+
app_resources_dir: Utf8PathBuf,
|
|
23
|
+
dry_run: bool,
|
|
24
|
+
status: PackageStatus,
|
|
25
|
+
create_dirs: Vec<Utf8PathBuf>,
|
|
26
|
+
copy_steps: Vec<CopyStep>,
|
|
27
|
+
warnings: Vec<String>,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#[derive(Debug, Serialize)]
|
|
31
|
+
struct CopyStep {
|
|
32
|
+
from: Utf8PathBuf,
|
|
33
|
+
to: Utf8PathBuf,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#[derive(Debug, Serialize)]
|
|
37
|
+
#[serde(rename_all = "kebab-case")]
|
|
38
|
+
enum PackageStatus {
|
|
39
|
+
Planned,
|
|
40
|
+
Packaged,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
pub fn run(args: PackageArgs) -> Result<()> {
|
|
44
|
+
let snapshot = crate::project::inspect(&args.cwd)?;
|
|
45
|
+
let mut report = build_report(snapshot, &args)?;
|
|
46
|
+
|
|
47
|
+
if args.dry_run {
|
|
48
|
+
return print_report(&report, args.json);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
execute_package(&report, args.force)?;
|
|
52
|
+
report.status = PackageStatus::Packaged;
|
|
53
|
+
|
|
54
|
+
print_report(&report, args.json)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
pub(crate) fn build_report(snapshot: ProjectSnapshot, args: &PackageArgs) -> Result<PackageReport> {
|
|
58
|
+
let root = Path::new(snapshot.root.as_str());
|
|
59
|
+
let platform = args.platform.clone().unwrap_or_else(current_platform);
|
|
60
|
+
let arch = args.arch.clone().unwrap_or_else(current_arch);
|
|
61
|
+
let app_name = clean_app_name(
|
|
62
|
+
&args
|
|
63
|
+
.name
|
|
64
|
+
.clone()
|
|
65
|
+
.or_else(|| snapshot.name.clone())
|
|
66
|
+
.unwrap_or_else(|| "electron-app".to_string()),
|
|
67
|
+
);
|
|
68
|
+
let executable_name = executable_name(&app_name, &platform);
|
|
69
|
+
let artifact_name = sanitize_artifact_name(&app_name);
|
|
70
|
+
let output_dir = resolve_output_dir(root, &args.out_dir);
|
|
71
|
+
let package_root = output_dir.join(format!("{artifact_name}-{platform}-{arch}"));
|
|
72
|
+
let bundle_dir = bundle_dir(&package_root, &app_name, &platform);
|
|
73
|
+
let app_resources_dir = app_resources_dir(&bundle_dir, &platform);
|
|
74
|
+
let electron_dist = root.join("node_modules/electron/dist");
|
|
75
|
+
let electron_source = electron_source(&electron_dist, &platform);
|
|
76
|
+
|
|
77
|
+
let mut warnings = Vec::new();
|
|
78
|
+
if snapshot.package_json.is_none() {
|
|
79
|
+
warnings.push("No package.json found.".to_string());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if snapshot.electron_dependency.is_none() {
|
|
83
|
+
warnings.push("No electron dependency is declared in package.json.".to_string());
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if snapshot.main.is_none() {
|
|
87
|
+
warnings.push("No package.json main field found.".to_string());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if has_runtime_dependencies(&snapshot) {
|
|
91
|
+
warnings.push(
|
|
92
|
+
"Packaging production node_modules is not implemented yet; this project declares runtime dependencies."
|
|
93
|
+
.to_string(),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if !electron_source.exists() {
|
|
98
|
+
warnings.push(format!(
|
|
99
|
+
"Electron runtime was not found at {}.",
|
|
100
|
+
electron_source.display()
|
|
101
|
+
));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if platform != current_platform() {
|
|
105
|
+
warnings.push(format!(
|
|
106
|
+
"Cross-platform packaging is not implemented yet; this host can package {}.",
|
|
107
|
+
current_platform()
|
|
108
|
+
));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if arch != current_arch() {
|
|
112
|
+
warnings.push(format!(
|
|
113
|
+
"Cross-architecture packaging is not implemented yet; this host can package {}.",
|
|
114
|
+
current_arch()
|
|
115
|
+
));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let create_dirs = vec![package_root.clone(), app_resources_dir.clone()];
|
|
119
|
+
let copy_steps = [
|
|
120
|
+
(electron_source, bundle_dir.clone()),
|
|
121
|
+
(root.to_path_buf(), app_resources_dir.join("app")),
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
Ok(PackageReport {
|
|
125
|
+
project: snapshot,
|
|
126
|
+
app_name,
|
|
127
|
+
executable_name,
|
|
128
|
+
platform,
|
|
129
|
+
arch,
|
|
130
|
+
electron_dist: utf8_path(electron_dist)?,
|
|
131
|
+
output_dir: utf8_path(output_dir)?,
|
|
132
|
+
bundle_dir: utf8_path(bundle_dir)?,
|
|
133
|
+
app_resources_dir: utf8_path(app_resources_dir)?,
|
|
134
|
+
dry_run: args.dry_run,
|
|
135
|
+
status: PackageStatus::Planned,
|
|
136
|
+
create_dirs: create_dirs
|
|
137
|
+
.into_iter()
|
|
138
|
+
.map(utf8_path)
|
|
139
|
+
.collect::<Result<Vec<_>>>()?,
|
|
140
|
+
copy_steps: copy_steps
|
|
141
|
+
.into_iter()
|
|
142
|
+
.map(|(from, to)| {
|
|
143
|
+
Ok(CopyStep {
|
|
144
|
+
from: utf8_path(from)?,
|
|
145
|
+
to: utf8_path(to)?,
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
.collect::<Result<Vec<_>>>()?,
|
|
149
|
+
warnings,
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
pub(crate) fn execute_package(report: &PackageReport, force: bool) -> Result<()> {
|
|
154
|
+
if report.project.package_json.is_none() {
|
|
155
|
+
bail!("No package.json found. Run electron-cli package inside an Electron project.");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if report.project.electron_dependency.is_none() {
|
|
159
|
+
bail!("No electron dependency found. Install Electron before packaging the app.");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if has_runtime_dependencies(&report.project) {
|
|
163
|
+
bail!(
|
|
164
|
+
"Packaging production node_modules is not implemented yet. Remove runtime dependencies or wait for dependency pruning support."
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if report.platform != current_platform() {
|
|
169
|
+
bail!(
|
|
170
|
+
"Cross-platform packaging is not implemented yet. Requested {}, host is {}.",
|
|
171
|
+
report.platform,
|
|
172
|
+
current_platform()
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if report.arch != current_arch() {
|
|
177
|
+
bail!(
|
|
178
|
+
"Cross-architecture packaging is not implemented yet. Requested {}, host is {}.",
|
|
179
|
+
report.arch,
|
|
180
|
+
current_arch()
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let bundle_dir = Path::new(report.bundle_dir.as_str());
|
|
185
|
+
let package_root = package_root(bundle_dir, &report.platform);
|
|
186
|
+
let app_resources_dir = Path::new(report.app_resources_dir.as_str());
|
|
187
|
+
let app_dir = app_resources_dir.join("app");
|
|
188
|
+
|
|
189
|
+
if package_root.exists() {
|
|
190
|
+
if force {
|
|
191
|
+
fs::remove_dir_all(&package_root)
|
|
192
|
+
.with_context(|| format!("Could not remove {}", package_root.display()))?;
|
|
193
|
+
} else {
|
|
194
|
+
bail!(
|
|
195
|
+
"Package output already exists: {}. Use --force to overwrite it.",
|
|
196
|
+
package_root.display()
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let electron_source = Path::new(report.copy_steps[0].from.as_str());
|
|
202
|
+
if !electron_source.exists() {
|
|
203
|
+
bail!(
|
|
204
|
+
"Electron runtime was not found at {}. Run your package manager install first.",
|
|
205
|
+
electron_source.display()
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
fs::create_dir_all(&package_root)
|
|
210
|
+
.with_context(|| format!("Could not create {}", package_root.display()))?;
|
|
211
|
+
copy_recursively(electron_source, bundle_dir).with_context(|| {
|
|
212
|
+
format!(
|
|
213
|
+
"Could not copy Electron runtime to {}",
|
|
214
|
+
bundle_dir.display()
|
|
215
|
+
)
|
|
216
|
+
})?;
|
|
217
|
+
rename_runtime_executable(bundle_dir, &report.executable_name, &report.platform)?;
|
|
218
|
+
|
|
219
|
+
fs::create_dir_all(&app_dir)
|
|
220
|
+
.with_context(|| format!("Could not create {}", app_dir.display()))?;
|
|
221
|
+
copy_project_files(
|
|
222
|
+
Path::new(report.project.root.as_str()),
|
|
223
|
+
&app_dir,
|
|
224
|
+
Path::new(report.output_dir.as_str()),
|
|
225
|
+
)?;
|
|
226
|
+
|
|
227
|
+
Ok(())
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
fn print_report(report: &PackageReport, json: bool) -> Result<()> {
|
|
231
|
+
if json {
|
|
232
|
+
return output::json(report);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
println!("electron-cli package");
|
|
236
|
+
println!();
|
|
237
|
+
println!("Project");
|
|
238
|
+
println!(" root: {}", report.project.root);
|
|
239
|
+
match report.project.package_label() {
|
|
240
|
+
Some(label) => println!(" package: {label}"),
|
|
241
|
+
None => println!(" package: not found"),
|
|
242
|
+
}
|
|
243
|
+
println!(" app name: {}", report.app_name);
|
|
244
|
+
println!(" executable: {}", report.executable_name);
|
|
245
|
+
println!(" target: {} {}", report.platform, report.arch);
|
|
246
|
+
println!(" status: {}", report.status.as_str());
|
|
247
|
+
|
|
248
|
+
println!();
|
|
249
|
+
println!("Output");
|
|
250
|
+
println!(" {}", report.bundle_dir);
|
|
251
|
+
|
|
252
|
+
println!();
|
|
253
|
+
println!("Copy");
|
|
254
|
+
for step in &report.copy_steps {
|
|
255
|
+
println!(" {} -> {}", step.from, step.to);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if !report.warnings.is_empty() {
|
|
259
|
+
println!();
|
|
260
|
+
println!("Warnings");
|
|
261
|
+
for warning in &report.warnings {
|
|
262
|
+
println!(" {warning}");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
Ok(())
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
fn copy_project_files(source: &Path, destination: &Path, output_dir: &Path) -> Result<()> {
|
|
270
|
+
for entry in
|
|
271
|
+
fs::read_dir(source).with_context(|| format!("Could not read {}", source.display()))?
|
|
272
|
+
{
|
|
273
|
+
let entry = entry?;
|
|
274
|
+
let source_path = entry.path();
|
|
275
|
+
let file_name = entry.file_name();
|
|
276
|
+
let file_name = file_name.to_string_lossy();
|
|
277
|
+
|
|
278
|
+
if should_skip_project_entry(&source_path, &file_name, output_dir) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
let destination_path = destination.join(file_name.as_ref());
|
|
283
|
+
if source_path.is_dir() {
|
|
284
|
+
copy_project_files(&source_path, &destination_path, output_dir)?;
|
|
285
|
+
} else {
|
|
286
|
+
if let Some(parent) = destination_path.parent() {
|
|
287
|
+
fs::create_dir_all(parent)
|
|
288
|
+
.with_context(|| format!("Could not create {}", parent.display()))?;
|
|
289
|
+
}
|
|
290
|
+
fs::copy(&source_path, &destination_path).with_context(|| {
|
|
291
|
+
format!(
|
|
292
|
+
"Could not copy {} to {}",
|
|
293
|
+
source_path.display(),
|
|
294
|
+
destination_path.display()
|
|
295
|
+
)
|
|
296
|
+
})?;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
Ok(())
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
fn copy_recursively(source: &Path, destination: &Path) -> Result<()> {
|
|
304
|
+
if source.is_dir() {
|
|
305
|
+
fs::create_dir_all(destination)
|
|
306
|
+
.with_context(|| format!("Could not create {}", destination.display()))?;
|
|
307
|
+
|
|
308
|
+
for entry in
|
|
309
|
+
fs::read_dir(source).with_context(|| format!("Could not read {}", source.display()))?
|
|
310
|
+
{
|
|
311
|
+
let entry = entry?;
|
|
312
|
+
let source_path = entry.path();
|
|
313
|
+
let destination_path = destination.join(entry.file_name());
|
|
314
|
+
copy_recursively(&source_path, &destination_path)?;
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
if let Some(parent) = destination.parent() {
|
|
318
|
+
fs::create_dir_all(parent)
|
|
319
|
+
.with_context(|| format!("Could not create {}", parent.display()))?;
|
|
320
|
+
}
|
|
321
|
+
fs::copy(source, destination).with_context(|| {
|
|
322
|
+
format!(
|
|
323
|
+
"Could not copy {} to {}",
|
|
324
|
+
source.display(),
|
|
325
|
+
destination.display()
|
|
326
|
+
)
|
|
327
|
+
})?;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
Ok(())
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
fn should_skip_project_entry(source_path: &Path, file_name: &str, output_dir: &Path) -> bool {
|
|
334
|
+
if matches!(file_name, ".git" | "node_modules" | "target") {
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
same_path_or_inside(source_path, output_dir)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
fn same_path_or_inside(path: &Path, parent: &Path) -> bool {
|
|
342
|
+
match (path.canonicalize(), parent.canonicalize()) {
|
|
343
|
+
(Ok(path), Ok(parent)) => path == parent || path.starts_with(parent),
|
|
344
|
+
_ => false,
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
fn rename_runtime_executable(
|
|
349
|
+
bundle_dir: &Path,
|
|
350
|
+
executable_name: &str,
|
|
351
|
+
platform: &str,
|
|
352
|
+
) -> Result<()> {
|
|
353
|
+
if platform == "darwin" {
|
|
354
|
+
return Ok(());
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let current = if platform == "win32" {
|
|
358
|
+
bundle_dir.join("electron.exe")
|
|
359
|
+
} else {
|
|
360
|
+
bundle_dir.join("electron")
|
|
361
|
+
};
|
|
362
|
+
let target = bundle_dir.join(executable_name);
|
|
363
|
+
|
|
364
|
+
if current.exists() && current != target {
|
|
365
|
+
fs::rename(¤t, &target).with_context(|| {
|
|
366
|
+
format!(
|
|
367
|
+
"Could not rename {} to {}",
|
|
368
|
+
current.display(),
|
|
369
|
+
target.display()
|
|
370
|
+
)
|
|
371
|
+
})?;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
Ok(())
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
fn resolve_output_dir(root: &Path, out_dir: &Path) -> PathBuf {
|
|
378
|
+
if out_dir.is_absolute() {
|
|
379
|
+
out_dir.to_path_buf()
|
|
380
|
+
} else {
|
|
381
|
+
root.join(out_dir)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
fn electron_source(electron_dist: &Path, platform: &str) -> PathBuf {
|
|
386
|
+
if platform == "darwin" {
|
|
387
|
+
electron_dist.join("Electron.app")
|
|
388
|
+
} else {
|
|
389
|
+
electron_dist.to_path_buf()
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
fn bundle_dir(package_root: &Path, app_name: &str, platform: &str) -> PathBuf {
|
|
394
|
+
if platform == "darwin" {
|
|
395
|
+
package_root.join(format!("{app_name}.app"))
|
|
396
|
+
} else {
|
|
397
|
+
package_root.to_path_buf()
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
fn package_root(bundle_dir: &Path, platform: &str) -> PathBuf {
|
|
402
|
+
if platform == "darwin" {
|
|
403
|
+
bundle_dir
|
|
404
|
+
.parent()
|
|
405
|
+
.expect("macOS bundle should have package parent")
|
|
406
|
+
.to_path_buf()
|
|
407
|
+
} else {
|
|
408
|
+
bundle_dir.to_path_buf()
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
fn app_resources_dir(bundle_dir: &Path, platform: &str) -> PathBuf {
|
|
413
|
+
if platform == "darwin" {
|
|
414
|
+
bundle_dir.join("Contents/Resources")
|
|
415
|
+
} else {
|
|
416
|
+
bundle_dir.join("resources")
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
fn executable_name(app_name: &str, platform: &str) -> String {
|
|
421
|
+
let mut name = sanitize_artifact_name(app_name);
|
|
422
|
+
if platform == "win32" {
|
|
423
|
+
name.push_str(".exe");
|
|
424
|
+
}
|
|
425
|
+
name
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
fn clean_app_name(name: &str) -> String {
|
|
429
|
+
let cleaned = name
|
|
430
|
+
.chars()
|
|
431
|
+
.map(|char| {
|
|
432
|
+
if char.is_ascii_alphanumeric() || matches!(char, ' ' | '-' | '_' | '.') {
|
|
433
|
+
char
|
|
434
|
+
} else {
|
|
435
|
+
'-'
|
|
436
|
+
}
|
|
437
|
+
})
|
|
438
|
+
.collect::<String>()
|
|
439
|
+
.trim_matches([' ', '-', '.', '_'])
|
|
440
|
+
.to_string();
|
|
441
|
+
|
|
442
|
+
if cleaned.is_empty() {
|
|
443
|
+
"electron-app".to_string()
|
|
444
|
+
} else {
|
|
445
|
+
cleaned
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
fn sanitize_artifact_name(name: &str) -> String {
|
|
450
|
+
let sanitized = name
|
|
451
|
+
.to_ascii_lowercase()
|
|
452
|
+
.chars()
|
|
453
|
+
.map(|char| {
|
|
454
|
+
if char.is_ascii_alphanumeric() || matches!(char, '-' | '_' | '.') {
|
|
455
|
+
char
|
|
456
|
+
} else {
|
|
457
|
+
'-'
|
|
458
|
+
}
|
|
459
|
+
})
|
|
460
|
+
.collect::<String>()
|
|
461
|
+
.trim_matches(['-', '.', '_'])
|
|
462
|
+
.to_string();
|
|
463
|
+
|
|
464
|
+
if sanitized.is_empty() {
|
|
465
|
+
"electron-app".to_string()
|
|
466
|
+
} else {
|
|
467
|
+
sanitized
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
fn has_runtime_dependencies(snapshot: &ProjectSnapshot) -> bool {
|
|
472
|
+
!snapshot.dependencies.is_empty() || !snapshot.optional_dependencies.is_empty()
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
fn current_platform() -> String {
|
|
476
|
+
if cfg!(target_os = "macos") {
|
|
477
|
+
"darwin".to_string()
|
|
478
|
+
} else if cfg!(target_os = "windows") {
|
|
479
|
+
"win32".to_string()
|
|
480
|
+
} else {
|
|
481
|
+
"linux".to_string()
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
fn current_arch() -> String {
|
|
486
|
+
match std::env::consts::ARCH {
|
|
487
|
+
"aarch64" => "arm64".to_string(),
|
|
488
|
+
"x86_64" => "x64".to_string(),
|
|
489
|
+
arch => arch.to_string(),
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
fn utf8_path(path: PathBuf) -> Result<Utf8PathBuf> {
|
|
494
|
+
Utf8PathBuf::from_path_buf(path).map_err(|path| {
|
|
495
|
+
anyhow::anyhow!(
|
|
496
|
+
"Path contains invalid UTF-8 and cannot be represented in JSON: {}",
|
|
497
|
+
path.display()
|
|
498
|
+
)
|
|
499
|
+
})
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
impl PackageStatus {
|
|
503
|
+
fn as_str(&self) -> &'static str {
|
|
504
|
+
match self {
|
|
505
|
+
PackageStatus::Planned => "planned",
|
|
506
|
+
PackageStatus::Packaged => "packaged",
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
impl PackageReport {
|
|
512
|
+
pub(crate) fn project(&self) -> &ProjectSnapshot {
|
|
513
|
+
&self.project
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
pub(crate) fn mark_packaged(&mut self) {
|
|
517
|
+
self.status = PackageStatus::Packaged;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
pub(crate) fn app_name(&self) -> &str {
|
|
521
|
+
&self.app_name
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
pub(crate) fn artifact_stem(&self) -> String {
|
|
525
|
+
sanitize_artifact_name(&self.app_name)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
pub(crate) fn platform(&self) -> &str {
|
|
529
|
+
&self.platform
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
pub(crate) fn arch(&self) -> &str {
|
|
533
|
+
&self.arch
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
pub(crate) fn output_dir(&self) -> &Utf8PathBuf {
|
|
537
|
+
&self.output_dir
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
pub(crate) fn bundle_dir(&self) -> &Utf8PathBuf {
|
|
541
|
+
&self.bundle_dir
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
pub(crate) fn warnings(&self) -> &[String] {
|
|
545
|
+
&self.warnings
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
#[cfg(test)]
|
|
550
|
+
mod tests {
|
|
551
|
+
use super::*;
|
|
552
|
+
|
|
553
|
+
#[test]
|
|
554
|
+
fn plans_package_output_for_current_platform() {
|
|
555
|
+
let root = unique_temp_dir("plan");
|
|
556
|
+
write_package_json(&root);
|
|
557
|
+
write_fake_electron_dist(&root);
|
|
558
|
+
|
|
559
|
+
let args = PackageArgs {
|
|
560
|
+
cwd: root.clone(),
|
|
561
|
+
out_dir: PathBuf::from("out"),
|
|
562
|
+
name: None,
|
|
563
|
+
platform: None,
|
|
564
|
+
arch: None,
|
|
565
|
+
force: false,
|
|
566
|
+
dry_run: true,
|
|
567
|
+
json: true,
|
|
568
|
+
};
|
|
569
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
570
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
571
|
+
|
|
572
|
+
assert_eq!(report.app_name, "starter-app");
|
|
573
|
+
assert_eq!(report.platform, current_platform());
|
|
574
|
+
assert_eq!(report.arch, current_arch());
|
|
575
|
+
assert!(report.warnings.is_empty());
|
|
576
|
+
|
|
577
|
+
let _ = fs::remove_dir_all(root);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
#[test]
|
|
581
|
+
fn packages_fake_electron_runtime_and_app_files() {
|
|
582
|
+
let root = unique_temp_dir("execute");
|
|
583
|
+
write_package_json(&root);
|
|
584
|
+
write_app_file(&root);
|
|
585
|
+
write_fake_electron_dist(&root);
|
|
586
|
+
fs::create_dir_all(root.join("node_modules/ignored"))
|
|
587
|
+
.expect("node_modules should be created");
|
|
588
|
+
fs::write(root.join("node_modules/ignored/file.js"), "")
|
|
589
|
+
.expect("ignored node module should be written");
|
|
590
|
+
|
|
591
|
+
let args = PackageArgs {
|
|
592
|
+
cwd: root.clone(),
|
|
593
|
+
out_dir: PathBuf::from("out"),
|
|
594
|
+
name: Some("Starter App".to_string()),
|
|
595
|
+
platform: None,
|
|
596
|
+
arch: None,
|
|
597
|
+
force: false,
|
|
598
|
+
dry_run: false,
|
|
599
|
+
json: false,
|
|
600
|
+
};
|
|
601
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
602
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
603
|
+
|
|
604
|
+
execute_package(&report, false).expect("package should succeed");
|
|
605
|
+
|
|
606
|
+
let app_dir = Path::new(report.app_resources_dir.as_str()).join("app");
|
|
607
|
+
assert!(app_dir.join("package.json").exists());
|
|
608
|
+
assert!(app_dir.join("src/main.js").exists());
|
|
609
|
+
assert!(!app_dir.join("node_modules").exists());
|
|
610
|
+
|
|
611
|
+
if current_platform() == "darwin" {
|
|
612
|
+
assert!(Path::new(report.bundle_dir.as_str())
|
|
613
|
+
.join("Contents")
|
|
614
|
+
.exists());
|
|
615
|
+
} else {
|
|
616
|
+
assert!(Path::new(report.bundle_dir.as_str())
|
|
617
|
+
.join(report.executable_name)
|
|
618
|
+
.exists());
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
let _ = fs::remove_dir_all(root);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
#[test]
|
|
625
|
+
fn refuses_runtime_dependencies_until_pruning_exists() {
|
|
626
|
+
let root = unique_temp_dir("runtime-deps");
|
|
627
|
+
fs::write(
|
|
628
|
+
root.join("package.json"),
|
|
629
|
+
r#"{"name":"starter-app","version":"0.1.0","main":"src/main.js","dependencies":{"left-pad":"1.3.0"},"devDependencies":{"electron":"30.0.0"}}"#,
|
|
630
|
+
)
|
|
631
|
+
.expect("package.json should be written");
|
|
632
|
+
write_app_file(&root);
|
|
633
|
+
write_fake_electron_dist(&root);
|
|
634
|
+
|
|
635
|
+
let args = PackageArgs {
|
|
636
|
+
cwd: root.clone(),
|
|
637
|
+
out_dir: PathBuf::from("out"),
|
|
638
|
+
name: None,
|
|
639
|
+
platform: None,
|
|
640
|
+
arch: None,
|
|
641
|
+
force: false,
|
|
642
|
+
dry_run: false,
|
|
643
|
+
json: false,
|
|
644
|
+
};
|
|
645
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
646
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
647
|
+
|
|
648
|
+
assert!(report
|
|
649
|
+
.warnings
|
|
650
|
+
.contains(&"Packaging production node_modules is not implemented yet; this project declares runtime dependencies.".to_string()));
|
|
651
|
+
assert!(execute_package(&report, false).is_err());
|
|
652
|
+
|
|
653
|
+
let _ = fs::remove_dir_all(root);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
#[test]
|
|
657
|
+
fn cleans_scoped_package_names_for_bundle_paths() {
|
|
658
|
+
assert_eq!(clean_app_name("@scope/app"), "scope-app");
|
|
659
|
+
assert_eq!(sanitize_artifact_name("Starter App"), "starter-app");
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
fn write_package_json(root: &Path) {
|
|
663
|
+
fs::write(
|
|
664
|
+
root.join("package.json"),
|
|
665
|
+
r#"{"name":"starter-app","version":"0.1.0","main":"src/main.js","devDependencies":{"electron":"30.0.0"}}"#,
|
|
666
|
+
)
|
|
667
|
+
.expect("package.json should be written");
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
fn write_app_file(root: &Path) {
|
|
671
|
+
fs::create_dir_all(root.join("src")).expect("src should be created");
|
|
672
|
+
fs::write(root.join("src/main.js"), "console.log('hello');")
|
|
673
|
+
.expect("main file should be written");
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
fn write_fake_electron_dist(root: &Path) {
|
|
677
|
+
let dist = root.join("node_modules/electron/dist");
|
|
678
|
+
if current_platform() == "darwin" {
|
|
679
|
+
let app = dist.join("Electron.app/Contents/MacOS");
|
|
680
|
+
fs::create_dir_all(&app).expect("fake macOS electron app should be created");
|
|
681
|
+
fs::write(app.join("Electron"), "").expect("fake macOS binary should be written");
|
|
682
|
+
} else if current_platform() == "win32" {
|
|
683
|
+
fs::create_dir_all(&dist).expect("fake electron dist should be created");
|
|
684
|
+
fs::write(dist.join("electron.exe"), "").expect("fake exe should be written");
|
|
685
|
+
} else {
|
|
686
|
+
fs::create_dir_all(&dist).expect("fake electron dist should be created");
|
|
687
|
+
fs::write(dist.join("electron"), "").expect("fake binary should be written");
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
fn unique_temp_dir(label: &str) -> PathBuf {
|
|
692
|
+
let nanos = std::time::SystemTime::now()
|
|
693
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
694
|
+
.expect("clock should be after epoch")
|
|
695
|
+
.as_nanos();
|
|
696
|
+
let path = std::env::temp_dir().join(format!(
|
|
697
|
+
"electron-cli-package-{label}-{}-{nanos}",
|
|
698
|
+
std::process::id()
|
|
699
|
+
));
|
|
700
|
+
fs::create_dir_all(&path).expect("temp dir should be created");
|
|
701
|
+
path
|
|
702
|
+
}
|
|
703
|
+
}
|