electron-cli 0.3.0-alpha.5 → 0.3.0-alpha.7
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 +82 -1
- package/Cargo.toml +2 -1
- package/README.md +21 -3
- package/package.json +1 -1
- package/src/commands/init.rs +30 -0
- package/src/commands/package.rs +944 -25
package/src/commands/package.rs
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
use std::{
|
|
2
|
+
collections::{BTreeMap, BTreeSet, VecDeque},
|
|
2
3
|
fs,
|
|
3
4
|
path::{Path, PathBuf},
|
|
4
5
|
};
|
|
5
6
|
|
|
6
7
|
use anyhow::{bail, Context, Result};
|
|
7
8
|
use camino::Utf8PathBuf;
|
|
9
|
+
use plist::{Dictionary as PlistDictionary, Value as PlistValue};
|
|
8
10
|
use serde::Serialize;
|
|
11
|
+
use serde_json::Value as JsonValue;
|
|
9
12
|
|
|
10
13
|
use crate::{cli::PackageArgs, output, project::ProjectSnapshot};
|
|
11
14
|
|
|
@@ -14,6 +17,7 @@ pub(crate) struct PackageReport {
|
|
|
14
17
|
project: ProjectSnapshot,
|
|
15
18
|
app_name: String,
|
|
16
19
|
executable_name: String,
|
|
20
|
+
metadata: PackageMetadata,
|
|
17
21
|
platform: String,
|
|
18
22
|
arch: String,
|
|
19
23
|
electron_dist: Utf8PathBuf,
|
|
@@ -33,6 +37,24 @@ struct CopyStep {
|
|
|
33
37
|
to: Utf8PathBuf,
|
|
34
38
|
}
|
|
35
39
|
|
|
40
|
+
#[derive(Debug, Serialize)]
|
|
41
|
+
struct PackageMetadata {
|
|
42
|
+
bundle_identifier: String,
|
|
43
|
+
app_version: Option<String>,
|
|
44
|
+
build_version: Option<String>,
|
|
45
|
+
app_category_type: Option<String>,
|
|
46
|
+
app_copyright: Option<String>,
|
|
47
|
+
icon: Option<IconResource>,
|
|
48
|
+
extra_resources: Vec<CopyStep>,
|
|
49
|
+
darwin_dark_mode_support: bool,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#[derive(Debug, Serialize)]
|
|
53
|
+
struct IconResource {
|
|
54
|
+
from: Utf8PathBuf,
|
|
55
|
+
to: Utf8PathBuf,
|
|
56
|
+
}
|
|
57
|
+
|
|
36
58
|
#[derive(Debug, Serialize)]
|
|
37
59
|
#[serde(rename_all = "kebab-case")]
|
|
38
60
|
enum PackageStatus {
|
|
@@ -40,6 +62,27 @@ enum PackageStatus {
|
|
|
40
62
|
Packaged,
|
|
41
63
|
}
|
|
42
64
|
|
|
65
|
+
#[derive(Debug, Default)]
|
|
66
|
+
struct PackageJsonConfig {
|
|
67
|
+
product_name: Option<String>,
|
|
68
|
+
app_version: Option<String>,
|
|
69
|
+
packager: PackagerConfig,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[derive(Debug, Default)]
|
|
73
|
+
struct PackagerConfig {
|
|
74
|
+
name: Option<String>,
|
|
75
|
+
executable_name: Option<String>,
|
|
76
|
+
app_bundle_id: Option<String>,
|
|
77
|
+
app_category_type: Option<String>,
|
|
78
|
+
app_version: Option<String>,
|
|
79
|
+
build_version: Option<String>,
|
|
80
|
+
app_copyright: Option<String>,
|
|
81
|
+
icon: Vec<String>,
|
|
82
|
+
extra_resource: Vec<String>,
|
|
83
|
+
darwin_dark_mode_support: bool,
|
|
84
|
+
}
|
|
85
|
+
|
|
43
86
|
pub fn run(args: PackageArgs) -> Result<()> {
|
|
44
87
|
let snapshot = crate::project::inspect(&args.cwd)?;
|
|
45
88
|
let mut report = build_report(snapshot, &args)?;
|
|
@@ -56,16 +99,25 @@ pub fn run(args: PackageArgs) -> Result<()> {
|
|
|
56
99
|
|
|
57
100
|
pub(crate) fn build_report(snapshot: ProjectSnapshot, args: &PackageArgs) -> Result<PackageReport> {
|
|
58
101
|
let root = Path::new(snapshot.root.as_str());
|
|
102
|
+
let package_config = read_package_json_config(&snapshot)?;
|
|
59
103
|
let platform = args.platform.clone().unwrap_or_else(current_platform);
|
|
60
104
|
let arch = args.arch.clone().unwrap_or_else(current_arch);
|
|
61
105
|
let app_name = clean_app_name(
|
|
62
106
|
&args
|
|
63
107
|
.name
|
|
64
108
|
.clone()
|
|
109
|
+
.or_else(|| package_config.packager.name.clone())
|
|
110
|
+
.or_else(|| package_config.product_name.clone())
|
|
65
111
|
.or_else(|| snapshot.name.clone())
|
|
66
112
|
.unwrap_or_else(|| "electron-app".to_string()),
|
|
67
113
|
);
|
|
68
|
-
let
|
|
114
|
+
let executable_base = package_config
|
|
115
|
+
.packager
|
|
116
|
+
.executable_name
|
|
117
|
+
.clone()
|
|
118
|
+
.map(|name| clean_app_name(&name))
|
|
119
|
+
.unwrap_or_else(|| app_name.clone());
|
|
120
|
+
let executable_name = executable_name(&executable_base, &platform);
|
|
69
121
|
let artifact_name = sanitize_artifact_name(&app_name);
|
|
70
122
|
let output_dir = resolve_output_dir(root, &args.out_dir);
|
|
71
123
|
let package_root = output_dir.join(format!("{artifact_name}-{platform}-{arch}"));
|
|
@@ -73,6 +125,13 @@ pub(crate) fn build_report(snapshot: ProjectSnapshot, args: &PackageArgs) -> Res
|
|
|
73
125
|
let app_resources_dir = app_resources_dir(&bundle_dir, &platform);
|
|
74
126
|
let electron_dist = root.join("node_modules/electron/dist");
|
|
75
127
|
let electron_source = electron_source(&electron_dist, &platform);
|
|
128
|
+
let (metadata, metadata_warnings) = package_metadata(
|
|
129
|
+
root,
|
|
130
|
+
&package_config,
|
|
131
|
+
&artifact_name,
|
|
132
|
+
&app_resources_dir,
|
|
133
|
+
&platform,
|
|
134
|
+
)?;
|
|
76
135
|
|
|
77
136
|
let mut warnings = Vec::new();
|
|
78
137
|
if snapshot.package_json.is_none() {
|
|
@@ -87,13 +146,6 @@ pub(crate) fn build_report(snapshot: ProjectSnapshot, args: &PackageArgs) -> Res
|
|
|
87
146
|
warnings.push("No package.json main field found.".to_string());
|
|
88
147
|
}
|
|
89
148
|
|
|
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
149
|
if !electron_source.exists() {
|
|
98
150
|
warnings.push(format!(
|
|
99
151
|
"Electron runtime was not found at {}.",
|
|
@@ -115,16 +167,38 @@ pub(crate) fn build_report(snapshot: ProjectSnapshot, args: &PackageArgs) -> Res
|
|
|
115
167
|
));
|
|
116
168
|
}
|
|
117
169
|
|
|
170
|
+
warnings.extend(runtime_dependency_warnings(root, &snapshot));
|
|
171
|
+
warnings.extend(metadata_warnings);
|
|
172
|
+
|
|
118
173
|
let create_dirs = vec![package_root.clone(), app_resources_dir.clone()];
|
|
119
|
-
let copy_steps = [
|
|
174
|
+
let mut copy_steps = vec![
|
|
120
175
|
(electron_source, bundle_dir.clone()),
|
|
121
176
|
(root.to_path_buf(), app_resources_dir.join("app")),
|
|
122
177
|
];
|
|
178
|
+
if has_runtime_dependencies(&snapshot) {
|
|
179
|
+
copy_steps.push((
|
|
180
|
+
root.join("node_modules"),
|
|
181
|
+
app_resources_dir.join("app/node_modules"),
|
|
182
|
+
));
|
|
183
|
+
}
|
|
184
|
+
if let Some(icon) = &metadata.icon {
|
|
185
|
+
copy_steps.push((
|
|
186
|
+
Path::new(icon.from.as_str()).to_path_buf(),
|
|
187
|
+
Path::new(icon.to.as_str()).to_path_buf(),
|
|
188
|
+
));
|
|
189
|
+
}
|
|
190
|
+
for resource in &metadata.extra_resources {
|
|
191
|
+
copy_steps.push((
|
|
192
|
+
Path::new(resource.from.as_str()).to_path_buf(),
|
|
193
|
+
Path::new(resource.to.as_str()).to_path_buf(),
|
|
194
|
+
));
|
|
195
|
+
}
|
|
123
196
|
|
|
124
197
|
Ok(PackageReport {
|
|
125
198
|
project: snapshot,
|
|
126
199
|
app_name,
|
|
127
200
|
executable_name,
|
|
201
|
+
metadata,
|
|
128
202
|
platform,
|
|
129
203
|
arch,
|
|
130
204
|
electron_dist: utf8_path(electron_dist)?,
|
|
@@ -159,12 +233,6 @@ pub(crate) fn execute_package(report: &PackageReport, force: bool) -> Result<()>
|
|
|
159
233
|
bail!("No electron dependency found. Install Electron before packaging the app.");
|
|
160
234
|
}
|
|
161
235
|
|
|
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
236
|
if report.platform != current_platform() {
|
|
169
237
|
bail!(
|
|
170
238
|
"Cross-platform packaging is not implemented yet. Requested {}, host is {}.",
|
|
@@ -215,6 +283,8 @@ pub(crate) fn execute_package(report: &PackageReport, force: bool) -> Result<()>
|
|
|
215
283
|
)
|
|
216
284
|
})?;
|
|
217
285
|
rename_runtime_executable(bundle_dir, &report.executable_name, &report.platform)?;
|
|
286
|
+
apply_package_metadata(report)?;
|
|
287
|
+
copy_package_resources(report)?;
|
|
218
288
|
|
|
219
289
|
fs::create_dir_all(&app_dir)
|
|
220
290
|
.with_context(|| format!("Could not create {}", app_dir.display()))?;
|
|
@@ -223,6 +293,11 @@ pub(crate) fn execute_package(report: &PackageReport, force: bool) -> Result<()>
|
|
|
223
293
|
&app_dir,
|
|
224
294
|
Path::new(report.output_dir.as_str()),
|
|
225
295
|
)?;
|
|
296
|
+
copy_runtime_dependencies(
|
|
297
|
+
Path::new(report.project.root.as_str()),
|
|
298
|
+
&app_dir,
|
|
299
|
+
&report.project,
|
|
300
|
+
)?;
|
|
226
301
|
|
|
227
302
|
Ok(())
|
|
228
303
|
}
|
|
@@ -242,6 +317,10 @@ fn print_report(report: &PackageReport, json: bool) -> Result<()> {
|
|
|
242
317
|
}
|
|
243
318
|
println!(" app name: {}", report.app_name);
|
|
244
319
|
println!(" executable: {}", report.executable_name);
|
|
320
|
+
println!(" bundle id: {}", report.metadata.bundle_identifier);
|
|
321
|
+
if let Some(version) = &report.metadata.app_version {
|
|
322
|
+
println!(" app version: {version}");
|
|
323
|
+
}
|
|
245
324
|
println!(" target: {} {}", report.platform, report.arch);
|
|
246
325
|
println!(" status: {}", report.status.as_str());
|
|
247
326
|
|
|
@@ -266,6 +345,290 @@ fn print_report(report: &PackageReport, json: bool) -> Result<()> {
|
|
|
266
345
|
Ok(())
|
|
267
346
|
}
|
|
268
347
|
|
|
348
|
+
fn read_package_json_config(snapshot: &ProjectSnapshot) -> Result<PackageJsonConfig> {
|
|
349
|
+
let Some(package_json_path) = &snapshot.package_json else {
|
|
350
|
+
return Ok(PackageJsonConfig::default());
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
let package_json_path = Path::new(package_json_path.as_str());
|
|
354
|
+
let raw = fs::read_to_string(package_json_path)
|
|
355
|
+
.with_context(|| format!("Could not read {}", package_json_path.display()))?;
|
|
356
|
+
let package = serde_json::from_str::<JsonValue>(&raw)
|
|
357
|
+
.with_context(|| format!("Could not parse {}", package_json_path.display()))?;
|
|
358
|
+
|
|
359
|
+
let mut packager = PackagerConfig::default();
|
|
360
|
+
if let Some(config) = package
|
|
361
|
+
.get("config")
|
|
362
|
+
.and_then(|config| config.get("forge"))
|
|
363
|
+
.and_then(|forge| forge.get("packagerConfig"))
|
|
364
|
+
{
|
|
365
|
+
packager.merge(parse_packager_config(config));
|
|
366
|
+
}
|
|
367
|
+
if let Some(config) = package.get("electronPackagerConfig") {
|
|
368
|
+
packager.merge(parse_packager_config(config));
|
|
369
|
+
}
|
|
370
|
+
if let Some(config) = package
|
|
371
|
+
.get("electronCli")
|
|
372
|
+
.or_else(|| package.get("electron-cli"))
|
|
373
|
+
.and_then(|config| config.get("packagerConfig"))
|
|
374
|
+
{
|
|
375
|
+
packager.merge(parse_packager_config(config));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
Ok(PackageJsonConfig {
|
|
379
|
+
product_name: package
|
|
380
|
+
.get("productName")
|
|
381
|
+
.and_then(JsonValue::as_str)
|
|
382
|
+
.map(ToOwned::to_owned),
|
|
383
|
+
app_version: package
|
|
384
|
+
.get("version")
|
|
385
|
+
.and_then(JsonValue::as_str)
|
|
386
|
+
.map(ToOwned::to_owned),
|
|
387
|
+
packager,
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
fn parse_packager_config(value: &JsonValue) -> PackagerConfig {
|
|
392
|
+
PackagerConfig {
|
|
393
|
+
name: string_value(value, "name"),
|
|
394
|
+
executable_name: string_value(value, "executableName"),
|
|
395
|
+
app_bundle_id: string_value(value, "appBundleId"),
|
|
396
|
+
app_category_type: string_value(value, "appCategoryType"),
|
|
397
|
+
app_version: string_value(value, "appVersion"),
|
|
398
|
+
build_version: string_value(value, "buildVersion"),
|
|
399
|
+
app_copyright: string_value(value, "appCopyright"),
|
|
400
|
+
icon: string_list(value.get("icon")),
|
|
401
|
+
extra_resource: string_list(value.get("extraResource")),
|
|
402
|
+
darwin_dark_mode_support: value
|
|
403
|
+
.get("darwinDarkModeSupport")
|
|
404
|
+
.and_then(JsonValue::as_bool)
|
|
405
|
+
.unwrap_or(false),
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
fn string_value(value: &JsonValue, key: &str) -> Option<String> {
|
|
410
|
+
value
|
|
411
|
+
.get(key)
|
|
412
|
+
.and_then(JsonValue::as_str)
|
|
413
|
+
.map(ToOwned::to_owned)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
fn string_list(value: Option<&JsonValue>) -> Vec<String> {
|
|
417
|
+
match value {
|
|
418
|
+
Some(JsonValue::String(value)) => vec![value.clone()],
|
|
419
|
+
Some(JsonValue::Array(values)) => values
|
|
420
|
+
.iter()
|
|
421
|
+
.filter_map(JsonValue::as_str)
|
|
422
|
+
.map(ToOwned::to_owned)
|
|
423
|
+
.collect(),
|
|
424
|
+
_ => Vec::new(),
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
fn package_metadata(
|
|
429
|
+
root: &Path,
|
|
430
|
+
config: &PackageJsonConfig,
|
|
431
|
+
artifact_name: &str,
|
|
432
|
+
app_resources_dir: &Path,
|
|
433
|
+
platform: &str,
|
|
434
|
+
) -> Result<(PackageMetadata, Vec<String>)> {
|
|
435
|
+
let mut warnings = Vec::new();
|
|
436
|
+
let icon = resolve_icon_resource(
|
|
437
|
+
root,
|
|
438
|
+
&config.packager.icon,
|
|
439
|
+
artifact_name,
|
|
440
|
+
app_resources_dir,
|
|
441
|
+
platform,
|
|
442
|
+
&mut warnings,
|
|
443
|
+
)?;
|
|
444
|
+
let extra_resources = resolve_extra_resources(
|
|
445
|
+
root,
|
|
446
|
+
&config.packager.extra_resource,
|
|
447
|
+
app_resources_dir,
|
|
448
|
+
&mut warnings,
|
|
449
|
+
)?;
|
|
450
|
+
let app_version = config
|
|
451
|
+
.packager
|
|
452
|
+
.app_version
|
|
453
|
+
.clone()
|
|
454
|
+
.or_else(|| config.app_version.clone());
|
|
455
|
+
|
|
456
|
+
Ok((
|
|
457
|
+
PackageMetadata {
|
|
458
|
+
bundle_identifier: config
|
|
459
|
+
.packager
|
|
460
|
+
.app_bundle_id
|
|
461
|
+
.clone()
|
|
462
|
+
.unwrap_or_else(|| default_bundle_identifier(artifact_name)),
|
|
463
|
+
app_version: app_version.clone(),
|
|
464
|
+
build_version: config
|
|
465
|
+
.packager
|
|
466
|
+
.build_version
|
|
467
|
+
.clone()
|
|
468
|
+
.or_else(|| app_version.clone()),
|
|
469
|
+
app_category_type: config.packager.app_category_type.clone(),
|
|
470
|
+
app_copyright: config.packager.app_copyright.clone(),
|
|
471
|
+
icon,
|
|
472
|
+
extra_resources,
|
|
473
|
+
darwin_dark_mode_support: config.packager.darwin_dark_mode_support,
|
|
474
|
+
},
|
|
475
|
+
warnings,
|
|
476
|
+
))
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
fn resolve_icon_resource(
|
|
480
|
+
root: &Path,
|
|
481
|
+
configured_icons: &[String],
|
|
482
|
+
artifact_name: &str,
|
|
483
|
+
app_resources_dir: &Path,
|
|
484
|
+
platform: &str,
|
|
485
|
+
warnings: &mut Vec<String>,
|
|
486
|
+
) -> Result<Option<IconResource>> {
|
|
487
|
+
let candidates = configured_icons
|
|
488
|
+
.iter()
|
|
489
|
+
.filter_map(|icon| icon_candidate(root, icon, platform))
|
|
490
|
+
.collect::<Vec<_>>();
|
|
491
|
+
let source = if platform == "darwin" {
|
|
492
|
+
candidates
|
|
493
|
+
.iter()
|
|
494
|
+
.find(|candidate| candidate.exists() && path_extension(candidate) == Some("icns"))
|
|
495
|
+
.cloned()
|
|
496
|
+
.or_else(|| {
|
|
497
|
+
candidates
|
|
498
|
+
.iter()
|
|
499
|
+
.find(|candidate| candidate.exists())
|
|
500
|
+
.cloned()
|
|
501
|
+
})
|
|
502
|
+
} else {
|
|
503
|
+
candidates
|
|
504
|
+
.iter()
|
|
505
|
+
.find(|candidate| candidate.exists())
|
|
506
|
+
.cloned()
|
|
507
|
+
};
|
|
508
|
+
let Some(source) = source else {
|
|
509
|
+
if let Some(first) = configured_icons.first() {
|
|
510
|
+
let expected = icon_candidate(root, first, platform)
|
|
511
|
+
.unwrap_or_else(|| resolve_project_path(root, first));
|
|
512
|
+
warnings.push(format!(
|
|
513
|
+
"Configured icon was not found for {platform}: {}.",
|
|
514
|
+
expected.display()
|
|
515
|
+
));
|
|
516
|
+
}
|
|
517
|
+
return Ok(None);
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
if platform == "darwin" && path_extension(&source) == Some("icon") {
|
|
521
|
+
warnings.push(
|
|
522
|
+
"macOS .icon files are not applied yet; provide an .icns icon for now.".to_string(),
|
|
523
|
+
);
|
|
524
|
+
return Ok(None);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if platform == "win32" {
|
|
528
|
+
warnings.push("Windows executable icon embedding is not implemented yet.".to_string());
|
|
529
|
+
return Ok(None);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if platform == "linux" {
|
|
533
|
+
warnings.push(
|
|
534
|
+
"Linux executable icons are not embedded; set the BrowserWindow icon in app code."
|
|
535
|
+
.to_string(),
|
|
536
|
+
);
|
|
537
|
+
return Ok(None);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
let extension = path_extension(&source).unwrap_or("icns");
|
|
541
|
+
let destination = app_resources_dir.join(format!("{artifact_name}.{extension}"));
|
|
542
|
+
|
|
543
|
+
Ok(Some(IconResource {
|
|
544
|
+
from: utf8_path(source)?,
|
|
545
|
+
to: utf8_path(destination)?,
|
|
546
|
+
}))
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
fn path_extension(path: &Path) -> Option<&str> {
|
|
550
|
+
path.extension().and_then(|extension| extension.to_str())
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
fn icon_candidate(root: &Path, configured_icon: &str, platform: &str) -> Option<PathBuf> {
|
|
554
|
+
if configured_icon.trim().is_empty() {
|
|
555
|
+
return None;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
let path = resolve_project_path(root, configured_icon);
|
|
559
|
+
if path.extension().is_some() {
|
|
560
|
+
return Some(path);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
let extension = match platform {
|
|
564
|
+
"darwin" => "icns",
|
|
565
|
+
"win32" => "ico",
|
|
566
|
+
"linux" => "png",
|
|
567
|
+
_ => return Some(path),
|
|
568
|
+
};
|
|
569
|
+
Some(path.with_extension(extension))
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
fn resolve_extra_resources(
|
|
573
|
+
root: &Path,
|
|
574
|
+
extra_resources: &[String],
|
|
575
|
+
app_resources_dir: &Path,
|
|
576
|
+
warnings: &mut Vec<String>,
|
|
577
|
+
) -> Result<Vec<CopyStep>> {
|
|
578
|
+
extra_resources
|
|
579
|
+
.iter()
|
|
580
|
+
.filter(|resource| !resource.trim().is_empty())
|
|
581
|
+
.map(|resource| {
|
|
582
|
+
let source = resolve_project_path(root, resource);
|
|
583
|
+
if !source.exists() {
|
|
584
|
+
warnings.push(format!(
|
|
585
|
+
"Configured extra resource does not exist and packaging will fail: {}.",
|
|
586
|
+
source.display()
|
|
587
|
+
));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
let file_name = source
|
|
591
|
+
.file_name()
|
|
592
|
+
.with_context(|| format!("Extra resource has no file name: {}", source.display()))?
|
|
593
|
+
.to_owned();
|
|
594
|
+
Ok(CopyStep {
|
|
595
|
+
from: utf8_path(source)?,
|
|
596
|
+
to: utf8_path(app_resources_dir.join(file_name))?,
|
|
597
|
+
})
|
|
598
|
+
})
|
|
599
|
+
.collect()
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
fn resolve_project_path(root: &Path, path: &str) -> PathBuf {
|
|
603
|
+
let path = Path::new(path);
|
|
604
|
+
if path.is_absolute() {
|
|
605
|
+
path.to_path_buf()
|
|
606
|
+
} else {
|
|
607
|
+
root.join(path)
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
fn default_bundle_identifier(artifact_name: &str) -> String {
|
|
612
|
+
let component = artifact_name
|
|
613
|
+
.chars()
|
|
614
|
+
.map(|char| {
|
|
615
|
+
if char.is_ascii_alphanumeric() || char == '-' {
|
|
616
|
+
char
|
|
617
|
+
} else {
|
|
618
|
+
'.'
|
|
619
|
+
}
|
|
620
|
+
})
|
|
621
|
+
.collect::<String>()
|
|
622
|
+
.trim_matches(['.', '-'])
|
|
623
|
+
.to_string();
|
|
624
|
+
let component = if component.is_empty() {
|
|
625
|
+
"electron-app".to_string()
|
|
626
|
+
} else {
|
|
627
|
+
component
|
|
628
|
+
};
|
|
629
|
+
format!("com.electron.{component}")
|
|
630
|
+
}
|
|
631
|
+
|
|
269
632
|
fn copy_project_files(source: &Path, destination: &Path, output_dir: &Path) -> Result<()> {
|
|
270
633
|
for entry in
|
|
271
634
|
fs::read_dir(source).with_context(|| format!("Could not read {}", source.display()))?
|
|
@@ -300,6 +663,273 @@ fn copy_project_files(source: &Path, destination: &Path, output_dir: &Path) -> R
|
|
|
300
663
|
Ok(())
|
|
301
664
|
}
|
|
302
665
|
|
|
666
|
+
#[derive(Debug)]
|
|
667
|
+
struct DependencyRequest {
|
|
668
|
+
name: String,
|
|
669
|
+
requested_by: Option<PathBuf>,
|
|
670
|
+
optional: bool,
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
fn copy_runtime_dependencies(
|
|
674
|
+
root: &Path,
|
|
675
|
+
app_dir: &Path,
|
|
676
|
+
snapshot: &ProjectSnapshot,
|
|
677
|
+
) -> Result<()> {
|
|
678
|
+
if !has_runtime_dependencies(snapshot) {
|
|
679
|
+
return Ok(());
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
let root_node_modules = root.join("node_modules");
|
|
683
|
+
let app_node_modules = app_dir.join("node_modules");
|
|
684
|
+
let mut queue = VecDeque::new();
|
|
685
|
+
let mut copied_paths = BTreeSet::new();
|
|
686
|
+
|
|
687
|
+
for name in snapshot.dependencies.keys() {
|
|
688
|
+
queue.push_back(DependencyRequest {
|
|
689
|
+
name: name.clone(),
|
|
690
|
+
requested_by: None,
|
|
691
|
+
optional: false,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
for name in snapshot.optional_dependencies.keys() {
|
|
696
|
+
queue.push_back(DependencyRequest {
|
|
697
|
+
name: name.clone(),
|
|
698
|
+
requested_by: None,
|
|
699
|
+
optional: true,
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
while let Some(request) = queue.pop_front() {
|
|
704
|
+
let Some(package_dir) = resolve_dependency_dir(
|
|
705
|
+
&root_node_modules,
|
|
706
|
+
request.requested_by.as_deref(),
|
|
707
|
+
&request.name,
|
|
708
|
+
) else {
|
|
709
|
+
if request.optional {
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
bail!(
|
|
714
|
+
"Runtime dependency '{}' is not installed. Run your package manager install first.",
|
|
715
|
+
request.name
|
|
716
|
+
);
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
let canonical_package_dir = package_dir
|
|
720
|
+
.canonicalize()
|
|
721
|
+
.with_context(|| format!("Could not resolve {}", package_dir.display()))?;
|
|
722
|
+
let canonical_root_node_modules = root_node_modules
|
|
723
|
+
.canonicalize()
|
|
724
|
+
.with_context(|| format!("Could not resolve {}", root_node_modules.display()))?;
|
|
725
|
+
if !copied_paths.insert(canonical_package_dir.clone()) {
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
let relative_path = canonical_package_dir
|
|
730
|
+
.strip_prefix(&canonical_root_node_modules)
|
|
731
|
+
.with_context(|| {
|
|
732
|
+
format!(
|
|
733
|
+
"Could not make dependency {} relative to {}",
|
|
734
|
+
canonical_package_dir.display(),
|
|
735
|
+
canonical_root_node_modules.display()
|
|
736
|
+
)
|
|
737
|
+
})?;
|
|
738
|
+
let destination = app_node_modules.join(relative_path);
|
|
739
|
+
copy_recursively(&canonical_package_dir, &destination).with_context(|| {
|
|
740
|
+
format!(
|
|
741
|
+
"Could not copy runtime dependency {} to {}",
|
|
742
|
+
canonical_package_dir.display(),
|
|
743
|
+
destination.display()
|
|
744
|
+
)
|
|
745
|
+
})?;
|
|
746
|
+
|
|
747
|
+
let package_json = read_dependency_package_json(&canonical_package_dir)?;
|
|
748
|
+
for name in string_map(package_json.get("dependencies")).keys() {
|
|
749
|
+
queue.push_back(DependencyRequest {
|
|
750
|
+
name: name.clone(),
|
|
751
|
+
requested_by: Some(canonical_package_dir.clone()),
|
|
752
|
+
optional: false,
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
for name in string_map(package_json.get("optionalDependencies")).keys() {
|
|
756
|
+
queue.push_back(DependencyRequest {
|
|
757
|
+
name: name.clone(),
|
|
758
|
+
requested_by: Some(canonical_package_dir.clone()),
|
|
759
|
+
optional: true,
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
Ok(())
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
fn apply_package_metadata(report: &PackageReport) -> Result<()> {
|
|
768
|
+
if report.platform == "darwin" {
|
|
769
|
+
apply_macos_metadata(report)?;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
Ok(())
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
fn apply_macos_metadata(report: &PackageReport) -> Result<()> {
|
|
776
|
+
let bundle_dir = Path::new(report.bundle_dir.as_str());
|
|
777
|
+
let info_plist_path = bundle_dir.join("Contents/Info.plist");
|
|
778
|
+
let mut dictionary = if info_plist_path.exists() {
|
|
779
|
+
match PlistValue::from_file(&info_plist_path)
|
|
780
|
+
.with_context(|| format!("Could not read {}", info_plist_path.display()))?
|
|
781
|
+
{
|
|
782
|
+
PlistValue::Dictionary(dictionary) => dictionary,
|
|
783
|
+
_ => bail!("{} is not a plist dictionary", info_plist_path.display()),
|
|
784
|
+
}
|
|
785
|
+
} else {
|
|
786
|
+
PlistDictionary::new()
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
set_plist_string(&mut dictionary, "CFBundleName", &report.app_name);
|
|
790
|
+
set_plist_string(&mut dictionary, "CFBundleDisplayName", &report.app_name);
|
|
791
|
+
set_plist_string(
|
|
792
|
+
&mut dictionary,
|
|
793
|
+
"CFBundleExecutable",
|
|
794
|
+
&report.executable_name,
|
|
795
|
+
);
|
|
796
|
+
set_plist_string(
|
|
797
|
+
&mut dictionary,
|
|
798
|
+
"CFBundleIdentifier",
|
|
799
|
+
&report.metadata.bundle_identifier,
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
if let Some(version) = &report.metadata.app_version {
|
|
803
|
+
set_plist_string(&mut dictionary, "CFBundleShortVersionString", version);
|
|
804
|
+
}
|
|
805
|
+
if let Some(version) = &report.metadata.build_version {
|
|
806
|
+
set_plist_string(&mut dictionary, "CFBundleVersion", version);
|
|
807
|
+
}
|
|
808
|
+
if let Some(category) = &report.metadata.app_category_type {
|
|
809
|
+
set_plist_string(&mut dictionary, "LSApplicationCategoryType", category);
|
|
810
|
+
}
|
|
811
|
+
if let Some(copyright) = &report.metadata.app_copyright {
|
|
812
|
+
set_plist_string(&mut dictionary, "NSHumanReadableCopyright", copyright);
|
|
813
|
+
}
|
|
814
|
+
if let Some(icon) = &report.metadata.icon {
|
|
815
|
+
let icon_name = Path::new(icon.to.as_str())
|
|
816
|
+
.file_name()
|
|
817
|
+
.and_then(|file_name| file_name.to_str())
|
|
818
|
+
.context("Icon destination has no file name")?;
|
|
819
|
+
set_plist_string(&mut dictionary, "CFBundleIconFile", icon_name);
|
|
820
|
+
}
|
|
821
|
+
if report.metadata.darwin_dark_mode_support {
|
|
822
|
+
dictionary.insert(
|
|
823
|
+
"NSRequiresAquaSystemAppearance".to_string(),
|
|
824
|
+
PlistValue::Boolean(false),
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if let Some(parent) = info_plist_path.parent() {
|
|
829
|
+
fs::create_dir_all(parent)
|
|
830
|
+
.with_context(|| format!("Could not create {}", parent.display()))?;
|
|
831
|
+
}
|
|
832
|
+
PlistValue::Dictionary(dictionary)
|
|
833
|
+
.to_file_xml(&info_plist_path)
|
|
834
|
+
.with_context(|| format!("Could not write {}", info_plist_path.display()))?;
|
|
835
|
+
|
|
836
|
+
Ok(())
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
fn set_plist_string(dictionary: &mut PlistDictionary, key: &str, value: &str) {
|
|
840
|
+
dictionary.insert(key.to_string(), PlistValue::String(value.to_string()));
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
fn copy_package_resources(report: &PackageReport) -> Result<()> {
|
|
844
|
+
if let Some(icon) = &report.metadata.icon {
|
|
845
|
+
copy_recursively(Path::new(icon.from.as_str()), Path::new(icon.to.as_str()))
|
|
846
|
+
.with_context(|| format!("Could not copy icon to {}", icon.to))?;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
for resource in &report.metadata.extra_resources {
|
|
850
|
+
copy_recursively(
|
|
851
|
+
Path::new(resource.from.as_str()),
|
|
852
|
+
Path::new(resource.to.as_str()),
|
|
853
|
+
)
|
|
854
|
+
.with_context(|| format!("Could not copy extra resource to {}", resource.to))?;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
Ok(())
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
fn runtime_dependency_warnings(root: &Path, snapshot: &ProjectSnapshot) -> Vec<String> {
|
|
861
|
+
let mut warnings = Vec::new();
|
|
862
|
+
let root_node_modules = root.join("node_modules");
|
|
863
|
+
|
|
864
|
+
for name in snapshot.dependencies.keys() {
|
|
865
|
+
if resolve_dependency_dir(&root_node_modules, None, name).is_none() {
|
|
866
|
+
warnings.push(format!(
|
|
867
|
+
"Runtime dependency is not installed and packaging will fail: {name}."
|
|
868
|
+
));
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
for name in snapshot.optional_dependencies.keys() {
|
|
873
|
+
if resolve_dependency_dir(&root_node_modules, None, name).is_none() {
|
|
874
|
+
warnings.push(format!(
|
|
875
|
+
"Optional runtime dependency is not installed and will be skipped: {name}."
|
|
876
|
+
));
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
warnings
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
fn resolve_dependency_dir(
|
|
884
|
+
root_node_modules: &Path,
|
|
885
|
+
requested_by: Option<&Path>,
|
|
886
|
+
name: &str,
|
|
887
|
+
) -> Option<PathBuf> {
|
|
888
|
+
let relative_path = dependency_relative_path(name);
|
|
889
|
+
|
|
890
|
+
if let Some(requested_by) = requested_by {
|
|
891
|
+
let nested = requested_by.join("node_modules").join(&relative_path);
|
|
892
|
+
if nested.exists() {
|
|
893
|
+
return Some(nested);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
let hoisted = root_node_modules.join(relative_path);
|
|
898
|
+
hoisted.exists().then_some(hoisted)
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
fn dependency_relative_path(name: &str) -> PathBuf {
|
|
902
|
+
let mut path = PathBuf::new();
|
|
903
|
+
for part in name.split('/') {
|
|
904
|
+
if !part.is_empty() {
|
|
905
|
+
path.push(part);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
path
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
fn read_dependency_package_json(package_dir: &Path) -> Result<JsonValue> {
|
|
912
|
+
let package_json_path = package_dir.join("package.json");
|
|
913
|
+
let raw = fs::read_to_string(&package_json_path)
|
|
914
|
+
.with_context(|| format!("Could not read {}", package_json_path.display()))?;
|
|
915
|
+
serde_json::from_str::<JsonValue>(&raw)
|
|
916
|
+
.with_context(|| format!("Could not parse {}", package_json_path.display()))
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
fn string_map(value: Option<&JsonValue>) -> BTreeMap<String, String> {
|
|
920
|
+
value
|
|
921
|
+
.and_then(JsonValue::as_object)
|
|
922
|
+
.map(|object| {
|
|
923
|
+
object
|
|
924
|
+
.iter()
|
|
925
|
+
.filter_map(|(key, value)| {
|
|
926
|
+
value.as_str().map(|value| (key.clone(), value.to_string()))
|
|
927
|
+
})
|
|
928
|
+
.collect()
|
|
929
|
+
})
|
|
930
|
+
.unwrap_or_default()
|
|
931
|
+
}
|
|
932
|
+
|
|
303
933
|
fn copy_recursively(source: &Path, destination: &Path) -> Result<()> {
|
|
304
934
|
if source.is_dir() {
|
|
305
935
|
fs::create_dir_all(destination)
|
|
@@ -350,16 +980,18 @@ fn rename_runtime_executable(
|
|
|
350
980
|
executable_name: &str,
|
|
351
981
|
platform: &str,
|
|
352
982
|
) -> Result<()> {
|
|
353
|
-
if platform == "darwin" {
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
let current = if platform == "win32" {
|
|
983
|
+
let current = if platform == "darwin" {
|
|
984
|
+
bundle_dir.join("Contents/MacOS/Electron")
|
|
985
|
+
} else if platform == "win32" {
|
|
358
986
|
bundle_dir.join("electron.exe")
|
|
359
987
|
} else {
|
|
360
988
|
bundle_dir.join("electron")
|
|
361
989
|
};
|
|
362
|
-
let target =
|
|
990
|
+
let target = if platform == "darwin" {
|
|
991
|
+
bundle_dir.join("Contents/MacOS").join(executable_name)
|
|
992
|
+
} else {
|
|
993
|
+
bundle_dir.join(executable_name)
|
|
994
|
+
};
|
|
363
995
|
|
|
364
996
|
if current.exists() && current != target {
|
|
365
997
|
fs::rename(¤t, &target).with_context(|| {
|
|
@@ -508,6 +1140,30 @@ impl PackageStatus {
|
|
|
508
1140
|
}
|
|
509
1141
|
}
|
|
510
1142
|
|
|
1143
|
+
impl PackagerConfig {
|
|
1144
|
+
fn merge(&mut self, other: PackagerConfig) {
|
|
1145
|
+
self.name = other.name.or_else(|| self.name.take());
|
|
1146
|
+
self.executable_name = other
|
|
1147
|
+
.executable_name
|
|
1148
|
+
.or_else(|| self.executable_name.take());
|
|
1149
|
+
self.app_bundle_id = other.app_bundle_id.or_else(|| self.app_bundle_id.take());
|
|
1150
|
+
self.app_category_type = other
|
|
1151
|
+
.app_category_type
|
|
1152
|
+
.or_else(|| self.app_category_type.take());
|
|
1153
|
+
self.app_version = other.app_version.or_else(|| self.app_version.take());
|
|
1154
|
+
self.build_version = other.build_version.or_else(|| self.build_version.take());
|
|
1155
|
+
self.app_copyright = other.app_copyright.or_else(|| self.app_copyright.take());
|
|
1156
|
+
if !other.icon.is_empty() {
|
|
1157
|
+
self.icon = other.icon;
|
|
1158
|
+
}
|
|
1159
|
+
if !other.extra_resource.is_empty() {
|
|
1160
|
+
self.extra_resource = other.extra_resource;
|
|
1161
|
+
}
|
|
1162
|
+
self.darwin_dark_mode_support =
|
|
1163
|
+
other.darwin_dark_mode_support || self.darwin_dark_mode_support;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
511
1167
|
impl PackageReport {
|
|
512
1168
|
pub(crate) fn project(&self) -> &ProjectSnapshot {
|
|
513
1169
|
&self.project
|
|
@@ -622,15 +1278,26 @@ mod tests {
|
|
|
622
1278
|
}
|
|
623
1279
|
|
|
624
1280
|
#[test]
|
|
625
|
-
fn
|
|
1281
|
+
fn packages_runtime_dependency_closure_from_node_modules() {
|
|
626
1282
|
let root = unique_temp_dir("runtime-deps");
|
|
627
1283
|
fs::write(
|
|
628
1284
|
root.join("package.json"),
|
|
629
|
-
r#"{"name":"starter-app","version":"0.1.0","main":"src/main.js","dependencies":{"
|
|
1285
|
+
r#"{"name":"starter-app","version":"0.1.0","main":"src/main.js","dependencies":{"dep-a":"1.0.0"},"devDependencies":{"electron":"30.0.0","dev-only":"1.0.0"}}"#,
|
|
630
1286
|
)
|
|
631
1287
|
.expect("package.json should be written");
|
|
632
1288
|
write_app_file(&root);
|
|
633
1289
|
write_fake_electron_dist(&root);
|
|
1290
|
+
write_dependency_package(
|
|
1291
|
+
&root,
|
|
1292
|
+
"dep-a",
|
|
1293
|
+
r#"{"name":"dep-a","version":"1.0.0","dependencies":{"dep-b":"1.0.0"}}"#,
|
|
1294
|
+
);
|
|
1295
|
+
write_dependency_package(&root, "dep-b", r#"{"name":"dep-b","version":"1.0.0"}"#);
|
|
1296
|
+
write_dependency_package(
|
|
1297
|
+
&root,
|
|
1298
|
+
"dev-only",
|
|
1299
|
+
r#"{"name":"dev-only","version":"1.0.0"}"#,
|
|
1300
|
+
);
|
|
634
1301
|
|
|
635
1302
|
let args = PackageArgs {
|
|
636
1303
|
cwd: root.clone(),
|
|
@@ -645,18 +1312,221 @@ mod tests {
|
|
|
645
1312
|
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
646
1313
|
let report = build_report(snapshot, &args).expect("report should build");
|
|
647
1314
|
|
|
1315
|
+
assert!(report.warnings.is_empty());
|
|
1316
|
+
execute_package(&report, false).expect("package should succeed");
|
|
1317
|
+
|
|
1318
|
+
let app_node_modules = Path::new(report.app_resources_dir.as_str())
|
|
1319
|
+
.join("app")
|
|
1320
|
+
.join("node_modules");
|
|
1321
|
+
assert!(app_node_modules.join("dep-a/package.json").exists());
|
|
1322
|
+
assert!(app_node_modules.join("dep-b/package.json").exists());
|
|
1323
|
+
assert!(!app_node_modules.join("dev-only").exists());
|
|
1324
|
+
|
|
1325
|
+
let _ = fs::remove_dir_all(root);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
#[test]
|
|
1329
|
+
fn plans_packager_metadata_from_package_json() {
|
|
1330
|
+
let root = unique_temp_dir("metadata-plan");
|
|
1331
|
+
write_metadata_package_json(&root);
|
|
1332
|
+
write_app_file(&root);
|
|
1333
|
+
write_fake_electron_dist(&root);
|
|
1334
|
+
write_icon_and_resource_files(&root);
|
|
1335
|
+
|
|
1336
|
+
let args = PackageArgs {
|
|
1337
|
+
cwd: root.clone(),
|
|
1338
|
+
out_dir: PathBuf::from("out"),
|
|
1339
|
+
name: None,
|
|
1340
|
+
platform: None,
|
|
1341
|
+
arch: None,
|
|
1342
|
+
force: false,
|
|
1343
|
+
dry_run: true,
|
|
1344
|
+
json: true,
|
|
1345
|
+
};
|
|
1346
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
1347
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
1348
|
+
|
|
1349
|
+
assert_eq!(report.app_name, "Starter Pro");
|
|
1350
|
+
assert_eq!(
|
|
1351
|
+
report.executable_name,
|
|
1352
|
+
executable_name("StarterExec", &report.platform)
|
|
1353
|
+
);
|
|
1354
|
+
assert_eq!(report.metadata.bundle_identifier, "com.example.starter");
|
|
1355
|
+
assert_eq!(report.metadata.app_version.as_deref(), Some("2.3.4"));
|
|
1356
|
+
assert_eq!(report.metadata.build_version.as_deref(), Some("234"));
|
|
1357
|
+
assert_eq!(
|
|
1358
|
+
report.metadata.app_category_type.as_deref(),
|
|
1359
|
+
Some("public.app-category.developer-tools")
|
|
1360
|
+
);
|
|
1361
|
+
assert_eq!(
|
|
1362
|
+
report.metadata.app_copyright.as_deref(),
|
|
1363
|
+
Some("Copyright 2026 Example")
|
|
1364
|
+
);
|
|
1365
|
+
assert_eq!(report.metadata.extra_resources.len(), 1);
|
|
648
1366
|
assert!(report
|
|
649
|
-
.
|
|
650
|
-
.
|
|
1367
|
+
.copy_steps
|
|
1368
|
+
.iter()
|
|
1369
|
+
.any(|step| step.to.as_str().ends_with("config.json")));
|
|
1370
|
+
|
|
1371
|
+
if current_platform() == "darwin" {
|
|
1372
|
+
assert!(report.metadata.icon.is_some());
|
|
1373
|
+
assert!(report.warnings.is_empty());
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
let _ = fs::remove_dir_all(root);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
#[test]
|
|
1380
|
+
fn packages_macos_info_plist_metadata() {
|
|
1381
|
+
if current_platform() != "darwin" {
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
let root = unique_temp_dir("metadata-execute");
|
|
1386
|
+
write_metadata_package_json(&root);
|
|
1387
|
+
write_app_file(&root);
|
|
1388
|
+
write_fake_electron_dist(&root);
|
|
1389
|
+
write_icon_and_resource_files(&root);
|
|
1390
|
+
|
|
1391
|
+
let args = PackageArgs {
|
|
1392
|
+
cwd: root.clone(),
|
|
1393
|
+
out_dir: PathBuf::from("out"),
|
|
1394
|
+
name: None,
|
|
1395
|
+
platform: None,
|
|
1396
|
+
arch: None,
|
|
1397
|
+
force: false,
|
|
1398
|
+
dry_run: false,
|
|
1399
|
+
json: false,
|
|
1400
|
+
};
|
|
1401
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
1402
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
1403
|
+
|
|
1404
|
+
execute_package(&report, false).expect("package should succeed");
|
|
1405
|
+
|
|
1406
|
+
let bundle_dir = Path::new(report.bundle_dir.as_str());
|
|
1407
|
+
assert!(bundle_dir
|
|
1408
|
+
.join("Contents/MacOS")
|
|
1409
|
+
.join(&report.executable_name)
|
|
1410
|
+
.exists());
|
|
1411
|
+
assert!(bundle_dir
|
|
1412
|
+
.join("Contents/Resources/starter-pro.icns")
|
|
1413
|
+
.exists());
|
|
1414
|
+
assert!(bundle_dir.join("Contents/Resources/config.json").exists());
|
|
1415
|
+
|
|
1416
|
+
let plist = PlistValue::from_file(bundle_dir.join("Contents/Info.plist"))
|
|
1417
|
+
.expect("Info.plist should parse");
|
|
1418
|
+
let dictionary = plist
|
|
1419
|
+
.as_dictionary()
|
|
1420
|
+
.expect("Info.plist should be a dictionary");
|
|
1421
|
+
|
|
1422
|
+
assert_eq!(
|
|
1423
|
+
plist_string(dictionary, "CFBundleDisplayName"),
|
|
1424
|
+
Some("Starter Pro")
|
|
1425
|
+
);
|
|
1426
|
+
assert_eq!(
|
|
1427
|
+
plist_string(dictionary, "CFBundleExecutable"),
|
|
1428
|
+
Some(report.executable_name.as_str())
|
|
1429
|
+
);
|
|
1430
|
+
assert_eq!(
|
|
1431
|
+
plist_string(dictionary, "CFBundleIdentifier"),
|
|
1432
|
+
Some("com.example.starter")
|
|
1433
|
+
);
|
|
1434
|
+
assert_eq!(
|
|
1435
|
+
plist_string(dictionary, "CFBundleShortVersionString"),
|
|
1436
|
+
Some("2.3.4")
|
|
1437
|
+
);
|
|
1438
|
+
assert_eq!(plist_string(dictionary, "CFBundleVersion"), Some("234"));
|
|
1439
|
+
assert_eq!(
|
|
1440
|
+
plist_string(dictionary, "LSApplicationCategoryType"),
|
|
1441
|
+
Some("public.app-category.developer-tools")
|
|
1442
|
+
);
|
|
1443
|
+
assert_eq!(
|
|
1444
|
+
plist_string(dictionary, "CFBundleIconFile"),
|
|
1445
|
+
Some("starter-pro.icns")
|
|
1446
|
+
);
|
|
1447
|
+
assert_eq!(
|
|
1448
|
+
dictionary
|
|
1449
|
+
.get("NSRequiresAquaSystemAppearance")
|
|
1450
|
+
.and_then(PlistValue::as_boolean),
|
|
1451
|
+
Some(false)
|
|
1452
|
+
);
|
|
1453
|
+
|
|
1454
|
+
let _ = fs::remove_dir_all(root);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
#[test]
|
|
1458
|
+
fn missing_required_runtime_dependency_fails() {
|
|
1459
|
+
let root = unique_temp_dir("runtime-deps");
|
|
1460
|
+
fs::write(
|
|
1461
|
+
root.join("package.json"),
|
|
1462
|
+
r#"{"name":"starter-app","version":"0.1.0","main":"src/main.js","dependencies":{"left-pad":"1.3.0"},"devDependencies":{"electron":"30.0.0"}}"#,
|
|
1463
|
+
)
|
|
1464
|
+
.expect("package.json should be written");
|
|
1465
|
+
write_app_file(&root);
|
|
1466
|
+
write_fake_electron_dist(&root);
|
|
1467
|
+
|
|
1468
|
+
let args = PackageArgs {
|
|
1469
|
+
cwd: root.clone(),
|
|
1470
|
+
out_dir: PathBuf::from("out"),
|
|
1471
|
+
name: None,
|
|
1472
|
+
platform: None,
|
|
1473
|
+
arch: None,
|
|
1474
|
+
force: false,
|
|
1475
|
+
dry_run: false,
|
|
1476
|
+
json: false,
|
|
1477
|
+
};
|
|
1478
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
1479
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
1480
|
+
|
|
1481
|
+
assert!(report.warnings.contains(
|
|
1482
|
+
&"Runtime dependency is not installed and packaging will fail: left-pad.".to_string()
|
|
1483
|
+
));
|
|
651
1484
|
assert!(execute_package(&report, false).is_err());
|
|
652
1485
|
|
|
653
1486
|
let _ = fs::remove_dir_all(root);
|
|
654
1487
|
}
|
|
655
1488
|
|
|
1489
|
+
#[test]
|
|
1490
|
+
fn missing_optional_runtime_dependency_is_skipped() {
|
|
1491
|
+
let root = unique_temp_dir("optional-runtime-deps");
|
|
1492
|
+
fs::write(
|
|
1493
|
+
root.join("package.json"),
|
|
1494
|
+
r#"{"name":"starter-app","version":"0.1.0","main":"src/main.js","optionalDependencies":{"optional-native":"1.0.0"},"devDependencies":{"electron":"30.0.0"}}"#,
|
|
1495
|
+
)
|
|
1496
|
+
.expect("package.json should be written");
|
|
1497
|
+
write_app_file(&root);
|
|
1498
|
+
write_fake_electron_dist(&root);
|
|
1499
|
+
|
|
1500
|
+
let args = PackageArgs {
|
|
1501
|
+
cwd: root.clone(),
|
|
1502
|
+
out_dir: PathBuf::from("out"),
|
|
1503
|
+
name: None,
|
|
1504
|
+
platform: None,
|
|
1505
|
+
arch: None,
|
|
1506
|
+
force: false,
|
|
1507
|
+
dry_run: false,
|
|
1508
|
+
json: false,
|
|
1509
|
+
};
|
|
1510
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
1511
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
1512
|
+
|
|
1513
|
+
assert!(report.warnings.contains(
|
|
1514
|
+
&"Optional runtime dependency is not installed and will be skipped: optional-native."
|
|
1515
|
+
.to_string()
|
|
1516
|
+
));
|
|
1517
|
+
execute_package(&report, false).expect("optional dependency should be skipped");
|
|
1518
|
+
|
|
1519
|
+
let _ = fs::remove_dir_all(root);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
656
1522
|
#[test]
|
|
657
1523
|
fn cleans_scoped_package_names_for_bundle_paths() {
|
|
658
1524
|
assert_eq!(clean_app_name("@scope/app"), "scope-app");
|
|
659
1525
|
assert_eq!(sanitize_artifact_name("Starter App"), "starter-app");
|
|
1526
|
+
assert_eq!(
|
|
1527
|
+
dependency_relative_path("@scope/app"),
|
|
1528
|
+
PathBuf::from("@scope/app")
|
|
1529
|
+
);
|
|
660
1530
|
}
|
|
661
1531
|
|
|
662
1532
|
fn write_package_json(root: &Path) {
|
|
@@ -667,12 +1537,61 @@ mod tests {
|
|
|
667
1537
|
.expect("package.json should be written");
|
|
668
1538
|
}
|
|
669
1539
|
|
|
1540
|
+
fn write_metadata_package_json(root: &Path) {
|
|
1541
|
+
fs::write(
|
|
1542
|
+
root.join("package.json"),
|
|
1543
|
+
r#"{
|
|
1544
|
+
"name": "starter-app",
|
|
1545
|
+
"productName": "Starter Pro",
|
|
1546
|
+
"version": "2.3.4",
|
|
1547
|
+
"main": "src/main.js",
|
|
1548
|
+
"devDependencies": {
|
|
1549
|
+
"electron": "30.0.0"
|
|
1550
|
+
},
|
|
1551
|
+
"electronCli": {
|
|
1552
|
+
"packagerConfig": {
|
|
1553
|
+
"executableName": "StarterExec",
|
|
1554
|
+
"appBundleId": "com.example.starter",
|
|
1555
|
+
"appCategoryType": "public.app-category.developer-tools",
|
|
1556
|
+
"buildVersion": "234",
|
|
1557
|
+
"appCopyright": "Copyright 2026 Example",
|
|
1558
|
+
"icon": "assets/starter",
|
|
1559
|
+
"extraResource": "assets/config.json",
|
|
1560
|
+
"darwinDarkModeSupport": true
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
}"#,
|
|
1564
|
+
)
|
|
1565
|
+
.expect("package.json should be written");
|
|
1566
|
+
}
|
|
1567
|
+
|
|
670
1568
|
fn write_app_file(root: &Path) {
|
|
671
1569
|
fs::create_dir_all(root.join("src")).expect("src should be created");
|
|
672
1570
|
fs::write(root.join("src/main.js"), "console.log('hello');")
|
|
673
1571
|
.expect("main file should be written");
|
|
674
1572
|
}
|
|
675
1573
|
|
|
1574
|
+
fn write_dependency_package(root: &Path, name: &str, package_json: &str) {
|
|
1575
|
+
let package_dir = root
|
|
1576
|
+
.join("node_modules")
|
|
1577
|
+
.join(dependency_relative_path(name));
|
|
1578
|
+
fs::create_dir_all(&package_dir).expect("dependency package dir should be created");
|
|
1579
|
+
fs::write(package_dir.join("package.json"), package_json)
|
|
1580
|
+
.expect("dependency package.json should be written");
|
|
1581
|
+
fs::write(package_dir.join("index.js"), "module.exports = true;")
|
|
1582
|
+
.expect("dependency index should be written");
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
fn write_icon_and_resource_files(root: &Path) {
|
|
1586
|
+
fs::create_dir_all(root.join("assets")).expect("assets should be created");
|
|
1587
|
+
fs::write(root.join("assets/starter.icns"), b"icns").expect("icon should be written");
|
|
1588
|
+
fs::write(root.join("assets/config.json"), "{}").expect("resource should be written");
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
fn plist_string<'a>(dictionary: &'a PlistDictionary, key: &str) -> Option<&'a str> {
|
|
1592
|
+
dictionary.get(key).and_then(PlistValue::as_string)
|
|
1593
|
+
}
|
|
1594
|
+
|
|
676
1595
|
fn write_fake_electron_dist(root: &Path) {
|
|
677
1596
|
let dist = root.join("node_modules/electron/dist");
|
|
678
1597
|
if current_platform() == "darwin" {
|