electron-cli 0.3.0-alpha.6 → 0.3.0-alpha.8
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 +157 -1
- package/Cargo.toml +4 -1
- package/README.md +24 -5
- package/package.json +1 -1
- package/src/cli.rs +4 -2
- package/src/commands/init.rs +30 -0
- package/src/commands/make.rs +607 -9
- package/src/commands/package.rs +663 -12
package/src/commands/package.rs
CHANGED
|
@@ -6,8 +6,9 @@ use std::{
|
|
|
6
6
|
|
|
7
7
|
use anyhow::{bail, Context, Result};
|
|
8
8
|
use camino::Utf8PathBuf;
|
|
9
|
+
use plist::{Dictionary as PlistDictionary, Value as PlistValue};
|
|
9
10
|
use serde::Serialize;
|
|
10
|
-
use serde_json::Value;
|
|
11
|
+
use serde_json::Value as JsonValue;
|
|
11
12
|
|
|
12
13
|
use crate::{cli::PackageArgs, output, project::ProjectSnapshot};
|
|
13
14
|
|
|
@@ -16,6 +17,7 @@ pub(crate) struct PackageReport {
|
|
|
16
17
|
project: ProjectSnapshot,
|
|
17
18
|
app_name: String,
|
|
18
19
|
executable_name: String,
|
|
20
|
+
metadata: PackageMetadata,
|
|
19
21
|
platform: String,
|
|
20
22
|
arch: String,
|
|
21
23
|
electron_dist: Utf8PathBuf,
|
|
@@ -35,6 +37,24 @@ struct CopyStep {
|
|
|
35
37
|
to: Utf8PathBuf,
|
|
36
38
|
}
|
|
37
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
|
+
|
|
38
58
|
#[derive(Debug, Serialize)]
|
|
39
59
|
#[serde(rename_all = "kebab-case")]
|
|
40
60
|
enum PackageStatus {
|
|
@@ -42,6 +62,27 @@ enum PackageStatus {
|
|
|
42
62
|
Packaged,
|
|
43
63
|
}
|
|
44
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
|
+
|
|
45
86
|
pub fn run(args: PackageArgs) -> Result<()> {
|
|
46
87
|
let snapshot = crate::project::inspect(&args.cwd)?;
|
|
47
88
|
let mut report = build_report(snapshot, &args)?;
|
|
@@ -58,16 +99,25 @@ pub fn run(args: PackageArgs) -> Result<()> {
|
|
|
58
99
|
|
|
59
100
|
pub(crate) fn build_report(snapshot: ProjectSnapshot, args: &PackageArgs) -> Result<PackageReport> {
|
|
60
101
|
let root = Path::new(snapshot.root.as_str());
|
|
102
|
+
let package_config = read_package_json_config(&snapshot)?;
|
|
61
103
|
let platform = args.platform.clone().unwrap_or_else(current_platform);
|
|
62
104
|
let arch = args.arch.clone().unwrap_or_else(current_arch);
|
|
63
105
|
let app_name = clean_app_name(
|
|
64
106
|
&args
|
|
65
107
|
.name
|
|
66
108
|
.clone()
|
|
109
|
+
.or_else(|| package_config.packager.name.clone())
|
|
110
|
+
.or_else(|| package_config.product_name.clone())
|
|
67
111
|
.or_else(|| snapshot.name.clone())
|
|
68
112
|
.unwrap_or_else(|| "electron-app".to_string()),
|
|
69
113
|
);
|
|
70
|
-
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);
|
|
71
121
|
let artifact_name = sanitize_artifact_name(&app_name);
|
|
72
122
|
let output_dir = resolve_output_dir(root, &args.out_dir);
|
|
73
123
|
let package_root = output_dir.join(format!("{artifact_name}-{platform}-{arch}"));
|
|
@@ -75,6 +125,13 @@ pub(crate) fn build_report(snapshot: ProjectSnapshot, args: &PackageArgs) -> Res
|
|
|
75
125
|
let app_resources_dir = app_resources_dir(&bundle_dir, &platform);
|
|
76
126
|
let electron_dist = root.join("node_modules/electron/dist");
|
|
77
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
|
+
)?;
|
|
78
135
|
|
|
79
136
|
let mut warnings = Vec::new();
|
|
80
137
|
if snapshot.package_json.is_none() {
|
|
@@ -111,6 +168,7 @@ pub(crate) fn build_report(snapshot: ProjectSnapshot, args: &PackageArgs) -> Res
|
|
|
111
168
|
}
|
|
112
169
|
|
|
113
170
|
warnings.extend(runtime_dependency_warnings(root, &snapshot));
|
|
171
|
+
warnings.extend(metadata_warnings);
|
|
114
172
|
|
|
115
173
|
let create_dirs = vec![package_root.clone(), app_resources_dir.clone()];
|
|
116
174
|
let mut copy_steps = vec![
|
|
@@ -123,11 +181,24 @@ pub(crate) fn build_report(snapshot: ProjectSnapshot, args: &PackageArgs) -> Res
|
|
|
123
181
|
app_resources_dir.join("app/node_modules"),
|
|
124
182
|
));
|
|
125
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
|
+
}
|
|
126
196
|
|
|
127
197
|
Ok(PackageReport {
|
|
128
198
|
project: snapshot,
|
|
129
199
|
app_name,
|
|
130
200
|
executable_name,
|
|
201
|
+
metadata,
|
|
131
202
|
platform,
|
|
132
203
|
arch,
|
|
133
204
|
electron_dist: utf8_path(electron_dist)?,
|
|
@@ -212,6 +283,8 @@ pub(crate) fn execute_package(report: &PackageReport, force: bool) -> Result<()>
|
|
|
212
283
|
)
|
|
213
284
|
})?;
|
|
214
285
|
rename_runtime_executable(bundle_dir, &report.executable_name, &report.platform)?;
|
|
286
|
+
apply_package_metadata(report)?;
|
|
287
|
+
copy_package_resources(report)?;
|
|
215
288
|
|
|
216
289
|
fs::create_dir_all(&app_dir)
|
|
217
290
|
.with_context(|| format!("Could not create {}", app_dir.display()))?;
|
|
@@ -244,6 +317,10 @@ fn print_report(report: &PackageReport, json: bool) -> Result<()> {
|
|
|
244
317
|
}
|
|
245
318
|
println!(" app name: {}", report.app_name);
|
|
246
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
|
+
}
|
|
247
324
|
println!(" target: {} {}", report.platform, report.arch);
|
|
248
325
|
println!(" status: {}", report.status.as_str());
|
|
249
326
|
|
|
@@ -268,6 +345,290 @@ fn print_report(report: &PackageReport, json: bool) -> Result<()> {
|
|
|
268
345
|
Ok(())
|
|
269
346
|
}
|
|
270
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
|
+
|
|
271
632
|
fn copy_project_files(source: &Path, destination: &Path, output_dir: &Path) -> Result<()> {
|
|
272
633
|
for entry in
|
|
273
634
|
fs::read_dir(source).with_context(|| format!("Could not read {}", source.display()))?
|
|
@@ -403,6 +764,99 @@ fn copy_runtime_dependencies(
|
|
|
403
764
|
Ok(())
|
|
404
765
|
}
|
|
405
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
|
+
|
|
406
860
|
fn runtime_dependency_warnings(root: &Path, snapshot: &ProjectSnapshot) -> Vec<String> {
|
|
407
861
|
let mut warnings = Vec::new();
|
|
408
862
|
let root_node_modules = root.join("node_modules");
|
|
@@ -454,17 +908,17 @@ fn dependency_relative_path(name: &str) -> PathBuf {
|
|
|
454
908
|
path
|
|
455
909
|
}
|
|
456
910
|
|
|
457
|
-
fn read_dependency_package_json(package_dir: &Path) -> Result<
|
|
911
|
+
fn read_dependency_package_json(package_dir: &Path) -> Result<JsonValue> {
|
|
458
912
|
let package_json_path = package_dir.join("package.json");
|
|
459
913
|
let raw = fs::read_to_string(&package_json_path)
|
|
460
914
|
.with_context(|| format!("Could not read {}", package_json_path.display()))?;
|
|
461
|
-
serde_json::from_str::<
|
|
915
|
+
serde_json::from_str::<JsonValue>(&raw)
|
|
462
916
|
.with_context(|| format!("Could not parse {}", package_json_path.display()))
|
|
463
917
|
}
|
|
464
918
|
|
|
465
|
-
fn string_map(value: Option<&
|
|
919
|
+
fn string_map(value: Option<&JsonValue>) -> BTreeMap<String, String> {
|
|
466
920
|
value
|
|
467
|
-
.and_then(
|
|
921
|
+
.and_then(JsonValue::as_object)
|
|
468
922
|
.map(|object| {
|
|
469
923
|
object
|
|
470
924
|
.iter()
|
|
@@ -526,16 +980,18 @@ fn rename_runtime_executable(
|
|
|
526
980
|
executable_name: &str,
|
|
527
981
|
platform: &str,
|
|
528
982
|
) -> Result<()> {
|
|
529
|
-
if platform == "darwin" {
|
|
530
|
-
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
let current = if platform == "win32" {
|
|
983
|
+
let current = if platform == "darwin" {
|
|
984
|
+
bundle_dir.join("Contents/MacOS/Electron")
|
|
985
|
+
} else if platform == "win32" {
|
|
534
986
|
bundle_dir.join("electron.exe")
|
|
535
987
|
} else {
|
|
536
988
|
bundle_dir.join("electron")
|
|
537
989
|
};
|
|
538
|
-
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
|
+
};
|
|
539
995
|
|
|
540
996
|
if current.exists() && current != target {
|
|
541
997
|
fs::rename(¤t, &target).with_context(|| {
|
|
@@ -684,6 +1140,30 @@ impl PackageStatus {
|
|
|
684
1140
|
}
|
|
685
1141
|
}
|
|
686
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
|
+
|
|
687
1167
|
impl PackageReport {
|
|
688
1168
|
pub(crate) fn project(&self) -> &ProjectSnapshot {
|
|
689
1169
|
&self.project
|
|
@@ -697,6 +1177,10 @@ impl PackageReport {
|
|
|
697
1177
|
&self.app_name
|
|
698
1178
|
}
|
|
699
1179
|
|
|
1180
|
+
pub(crate) fn executable_name(&self) -> &str {
|
|
1181
|
+
&self.executable_name
|
|
1182
|
+
}
|
|
1183
|
+
|
|
700
1184
|
pub(crate) fn artifact_stem(&self) -> String {
|
|
701
1185
|
sanitize_artifact_name(&self.app_name)
|
|
702
1186
|
}
|
|
@@ -845,6 +1329,135 @@ mod tests {
|
|
|
845
1329
|
let _ = fs::remove_dir_all(root);
|
|
846
1330
|
}
|
|
847
1331
|
|
|
1332
|
+
#[test]
|
|
1333
|
+
fn plans_packager_metadata_from_package_json() {
|
|
1334
|
+
let root = unique_temp_dir("metadata-plan");
|
|
1335
|
+
write_metadata_package_json(&root);
|
|
1336
|
+
write_app_file(&root);
|
|
1337
|
+
write_fake_electron_dist(&root);
|
|
1338
|
+
write_icon_and_resource_files(&root);
|
|
1339
|
+
|
|
1340
|
+
let args = PackageArgs {
|
|
1341
|
+
cwd: root.clone(),
|
|
1342
|
+
out_dir: PathBuf::from("out"),
|
|
1343
|
+
name: None,
|
|
1344
|
+
platform: None,
|
|
1345
|
+
arch: None,
|
|
1346
|
+
force: false,
|
|
1347
|
+
dry_run: true,
|
|
1348
|
+
json: true,
|
|
1349
|
+
};
|
|
1350
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
1351
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
1352
|
+
|
|
1353
|
+
assert_eq!(report.app_name, "Starter Pro");
|
|
1354
|
+
assert_eq!(
|
|
1355
|
+
report.executable_name,
|
|
1356
|
+
executable_name("StarterExec", &report.platform)
|
|
1357
|
+
);
|
|
1358
|
+
assert_eq!(report.metadata.bundle_identifier, "com.example.starter");
|
|
1359
|
+
assert_eq!(report.metadata.app_version.as_deref(), Some("2.3.4"));
|
|
1360
|
+
assert_eq!(report.metadata.build_version.as_deref(), Some("234"));
|
|
1361
|
+
assert_eq!(
|
|
1362
|
+
report.metadata.app_category_type.as_deref(),
|
|
1363
|
+
Some("public.app-category.developer-tools")
|
|
1364
|
+
);
|
|
1365
|
+
assert_eq!(
|
|
1366
|
+
report.metadata.app_copyright.as_deref(),
|
|
1367
|
+
Some("Copyright 2026 Example")
|
|
1368
|
+
);
|
|
1369
|
+
assert_eq!(report.metadata.extra_resources.len(), 1);
|
|
1370
|
+
assert!(report
|
|
1371
|
+
.copy_steps
|
|
1372
|
+
.iter()
|
|
1373
|
+
.any(|step| step.to.as_str().ends_with("config.json")));
|
|
1374
|
+
|
|
1375
|
+
if current_platform() == "darwin" {
|
|
1376
|
+
assert!(report.metadata.icon.is_some());
|
|
1377
|
+
assert!(report.warnings.is_empty());
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
let _ = fs::remove_dir_all(root);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
#[test]
|
|
1384
|
+
fn packages_macos_info_plist_metadata() {
|
|
1385
|
+
if current_platform() != "darwin" {
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
let root = unique_temp_dir("metadata-execute");
|
|
1390
|
+
write_metadata_package_json(&root);
|
|
1391
|
+
write_app_file(&root);
|
|
1392
|
+
write_fake_electron_dist(&root);
|
|
1393
|
+
write_icon_and_resource_files(&root);
|
|
1394
|
+
|
|
1395
|
+
let args = PackageArgs {
|
|
1396
|
+
cwd: root.clone(),
|
|
1397
|
+
out_dir: PathBuf::from("out"),
|
|
1398
|
+
name: None,
|
|
1399
|
+
platform: None,
|
|
1400
|
+
arch: None,
|
|
1401
|
+
force: false,
|
|
1402
|
+
dry_run: false,
|
|
1403
|
+
json: false,
|
|
1404
|
+
};
|
|
1405
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
1406
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
1407
|
+
|
|
1408
|
+
execute_package(&report, false).expect("package should succeed");
|
|
1409
|
+
|
|
1410
|
+
let bundle_dir = Path::new(report.bundle_dir.as_str());
|
|
1411
|
+
assert!(bundle_dir
|
|
1412
|
+
.join("Contents/MacOS")
|
|
1413
|
+
.join(&report.executable_name)
|
|
1414
|
+
.exists());
|
|
1415
|
+
assert!(bundle_dir
|
|
1416
|
+
.join("Contents/Resources/starter-pro.icns")
|
|
1417
|
+
.exists());
|
|
1418
|
+
assert!(bundle_dir.join("Contents/Resources/config.json").exists());
|
|
1419
|
+
|
|
1420
|
+
let plist = PlistValue::from_file(bundle_dir.join("Contents/Info.plist"))
|
|
1421
|
+
.expect("Info.plist should parse");
|
|
1422
|
+
let dictionary = plist
|
|
1423
|
+
.as_dictionary()
|
|
1424
|
+
.expect("Info.plist should be a dictionary");
|
|
1425
|
+
|
|
1426
|
+
assert_eq!(
|
|
1427
|
+
plist_string(dictionary, "CFBundleDisplayName"),
|
|
1428
|
+
Some("Starter Pro")
|
|
1429
|
+
);
|
|
1430
|
+
assert_eq!(
|
|
1431
|
+
plist_string(dictionary, "CFBundleExecutable"),
|
|
1432
|
+
Some(report.executable_name.as_str())
|
|
1433
|
+
);
|
|
1434
|
+
assert_eq!(
|
|
1435
|
+
plist_string(dictionary, "CFBundleIdentifier"),
|
|
1436
|
+
Some("com.example.starter")
|
|
1437
|
+
);
|
|
1438
|
+
assert_eq!(
|
|
1439
|
+
plist_string(dictionary, "CFBundleShortVersionString"),
|
|
1440
|
+
Some("2.3.4")
|
|
1441
|
+
);
|
|
1442
|
+
assert_eq!(plist_string(dictionary, "CFBundleVersion"), Some("234"));
|
|
1443
|
+
assert_eq!(
|
|
1444
|
+
plist_string(dictionary, "LSApplicationCategoryType"),
|
|
1445
|
+
Some("public.app-category.developer-tools")
|
|
1446
|
+
);
|
|
1447
|
+
assert_eq!(
|
|
1448
|
+
plist_string(dictionary, "CFBundleIconFile"),
|
|
1449
|
+
Some("starter-pro.icns")
|
|
1450
|
+
);
|
|
1451
|
+
assert_eq!(
|
|
1452
|
+
dictionary
|
|
1453
|
+
.get("NSRequiresAquaSystemAppearance")
|
|
1454
|
+
.and_then(PlistValue::as_boolean),
|
|
1455
|
+
Some(false)
|
|
1456
|
+
);
|
|
1457
|
+
|
|
1458
|
+
let _ = fs::remove_dir_all(root);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
848
1461
|
#[test]
|
|
849
1462
|
fn missing_required_runtime_dependency_fails() {
|
|
850
1463
|
let root = unique_temp_dir("runtime-deps");
|
|
@@ -928,6 +1541,34 @@ mod tests {
|
|
|
928
1541
|
.expect("package.json should be written");
|
|
929
1542
|
}
|
|
930
1543
|
|
|
1544
|
+
fn write_metadata_package_json(root: &Path) {
|
|
1545
|
+
fs::write(
|
|
1546
|
+
root.join("package.json"),
|
|
1547
|
+
r#"{
|
|
1548
|
+
"name": "starter-app",
|
|
1549
|
+
"productName": "Starter Pro",
|
|
1550
|
+
"version": "2.3.4",
|
|
1551
|
+
"main": "src/main.js",
|
|
1552
|
+
"devDependencies": {
|
|
1553
|
+
"electron": "30.0.0"
|
|
1554
|
+
},
|
|
1555
|
+
"electronCli": {
|
|
1556
|
+
"packagerConfig": {
|
|
1557
|
+
"executableName": "StarterExec",
|
|
1558
|
+
"appBundleId": "com.example.starter",
|
|
1559
|
+
"appCategoryType": "public.app-category.developer-tools",
|
|
1560
|
+
"buildVersion": "234",
|
|
1561
|
+
"appCopyright": "Copyright 2026 Example",
|
|
1562
|
+
"icon": "assets/starter",
|
|
1563
|
+
"extraResource": "assets/config.json",
|
|
1564
|
+
"darwinDarkModeSupport": true
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
}"#,
|
|
1568
|
+
)
|
|
1569
|
+
.expect("package.json should be written");
|
|
1570
|
+
}
|
|
1571
|
+
|
|
931
1572
|
fn write_app_file(root: &Path) {
|
|
932
1573
|
fs::create_dir_all(root.join("src")).expect("src should be created");
|
|
933
1574
|
fs::write(root.join("src/main.js"), "console.log('hello');")
|
|
@@ -945,6 +1586,16 @@ mod tests {
|
|
|
945
1586
|
.expect("dependency index should be written");
|
|
946
1587
|
}
|
|
947
1588
|
|
|
1589
|
+
fn write_icon_and_resource_files(root: &Path) {
|
|
1590
|
+
fs::create_dir_all(root.join("assets")).expect("assets should be created");
|
|
1591
|
+
fs::write(root.join("assets/starter.icns"), b"icns").expect("icon should be written");
|
|
1592
|
+
fs::write(root.join("assets/config.json"), "{}").expect("resource should be written");
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
fn plist_string<'a>(dictionary: &'a PlistDictionary, key: &str) -> Option<&'a str> {
|
|
1596
|
+
dictionary.get(key).and_then(PlistValue::as_string)
|
|
1597
|
+
}
|
|
1598
|
+
|
|
948
1599
|
fn write_fake_electron_dist(root: &Path) {
|
|
949
1600
|
let dist = root.join("node_modules/electron/dist");
|
|
950
1601
|
if current_platform() == "darwin" {
|