electron-cli 0.3.0-alpha.6 → 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 +659 -12
package/Cargo.lock
CHANGED
|
@@ -64,6 +64,12 @@ version = "1.0.102"
|
|
|
64
64
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
65
65
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
|
66
66
|
|
|
67
|
+
[[package]]
|
|
68
|
+
name = "base64"
|
|
69
|
+
version = "0.22.1"
|
|
70
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
71
|
+
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
|
72
|
+
|
|
67
73
|
[[package]]
|
|
68
74
|
name = "camino"
|
|
69
75
|
version = "1.2.2"
|
|
@@ -134,13 +140,23 @@ dependencies = [
|
|
|
134
140
|
"cfg-if",
|
|
135
141
|
]
|
|
136
142
|
|
|
143
|
+
[[package]]
|
|
144
|
+
name = "deranged"
|
|
145
|
+
version = "0.5.8"
|
|
146
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
147
|
+
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
|
148
|
+
dependencies = [
|
|
149
|
+
"powerfmt",
|
|
150
|
+
]
|
|
151
|
+
|
|
137
152
|
[[package]]
|
|
138
153
|
name = "electron-cli"
|
|
139
|
-
version = "0.3.0-alpha.
|
|
154
|
+
version = "0.3.0-alpha.7"
|
|
140
155
|
dependencies = [
|
|
141
156
|
"anyhow",
|
|
142
157
|
"camino",
|
|
143
158
|
"clap",
|
|
159
|
+
"plist",
|
|
144
160
|
"serde",
|
|
145
161
|
"serde_json",
|
|
146
162
|
"zip",
|
|
@@ -212,12 +228,37 @@ dependencies = [
|
|
|
212
228
|
"simd-adler32",
|
|
213
229
|
]
|
|
214
230
|
|
|
231
|
+
[[package]]
|
|
232
|
+
name = "num-conv"
|
|
233
|
+
version = "0.2.2"
|
|
234
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
235
|
+
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
|
|
236
|
+
|
|
215
237
|
[[package]]
|
|
216
238
|
name = "once_cell_polyfill"
|
|
217
239
|
version = "1.70.2"
|
|
218
240
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
219
241
|
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
|
220
242
|
|
|
243
|
+
[[package]]
|
|
244
|
+
name = "plist"
|
|
245
|
+
version = "1.9.0"
|
|
246
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
247
|
+
checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1"
|
|
248
|
+
dependencies = [
|
|
249
|
+
"base64",
|
|
250
|
+
"indexmap",
|
|
251
|
+
"quick-xml",
|
|
252
|
+
"serde",
|
|
253
|
+
"time",
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
[[package]]
|
|
257
|
+
name = "powerfmt"
|
|
258
|
+
version = "0.2.0"
|
|
259
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
260
|
+
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
|
261
|
+
|
|
221
262
|
[[package]]
|
|
222
263
|
name = "proc-macro2"
|
|
223
264
|
version = "1.0.106"
|
|
@@ -227,6 +268,15 @@ dependencies = [
|
|
|
227
268
|
"unicode-ident",
|
|
228
269
|
]
|
|
229
270
|
|
|
271
|
+
[[package]]
|
|
272
|
+
name = "quick-xml"
|
|
273
|
+
version = "0.39.4"
|
|
274
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
275
|
+
checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e"
|
|
276
|
+
dependencies = [
|
|
277
|
+
"memchr",
|
|
278
|
+
]
|
|
279
|
+
|
|
230
280
|
[[package]]
|
|
231
281
|
name = "quote"
|
|
232
282
|
version = "1.0.45"
|
|
@@ -302,6 +352,37 @@ dependencies = [
|
|
|
302
352
|
"unicode-ident",
|
|
303
353
|
]
|
|
304
354
|
|
|
355
|
+
[[package]]
|
|
356
|
+
name = "time"
|
|
357
|
+
version = "0.3.47"
|
|
358
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
359
|
+
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
|
360
|
+
dependencies = [
|
|
361
|
+
"deranged",
|
|
362
|
+
"itoa",
|
|
363
|
+
"num-conv",
|
|
364
|
+
"powerfmt",
|
|
365
|
+
"serde_core",
|
|
366
|
+
"time-core",
|
|
367
|
+
"time-macros",
|
|
368
|
+
]
|
|
369
|
+
|
|
370
|
+
[[package]]
|
|
371
|
+
name = "time-core"
|
|
372
|
+
version = "0.1.8"
|
|
373
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
374
|
+
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
|
375
|
+
|
|
376
|
+
[[package]]
|
|
377
|
+
name = "time-macros"
|
|
378
|
+
version = "0.2.27"
|
|
379
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
380
|
+
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
|
|
381
|
+
dependencies = [
|
|
382
|
+
"num-conv",
|
|
383
|
+
"time-core",
|
|
384
|
+
]
|
|
385
|
+
|
|
305
386
|
[[package]]
|
|
306
387
|
name = "typed-path"
|
|
307
388
|
version = "0.12.3"
|
package/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "electron-cli"
|
|
3
|
-
version = "0.3.0-alpha.
|
|
3
|
+
version = "0.3.0-alpha.7"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
description = "Experimental Rust CLI for Electron project diagnostics and workflow automation"
|
|
6
6
|
license = "MIT"
|
|
@@ -12,4 +12,5 @@ camino = { version = "1.1", features = ["serde1"] }
|
|
|
12
12
|
clap = { version = "4.6", features = ["derive"] }
|
|
13
13
|
serde = { version = "1.0", features = ["derive"] }
|
|
14
14
|
serde_json = "1.0"
|
|
15
|
+
plist = "1"
|
|
15
16
|
zip = { version = "8.6.0", default-features = false, features = ["deflate-flate2-zlib-rs"] }
|
package/README.md
CHANGED
|
@@ -41,11 +41,29 @@ The Rust-native flow currently owns:
|
|
|
41
41
|
|
|
42
42
|
- `init --template minimal`: writes a local Electron starter without Electron Forge.
|
|
43
43
|
- `start`: launches the installed Electron runtime directly.
|
|
44
|
-
- `package`: copies the installed Electron runtime, app files,
|
|
44
|
+
- `package`: copies the installed Electron runtime, app files, installed production dependency closure, app metadata, macOS icon, and extra resources into a local app bundle for the current platform and architecture.
|
|
45
45
|
- `make`: runs `package` and writes a ZIP distributable under `out/make/zip/<platform>/<arch>/`.
|
|
46
46
|
- `publish`: runs `make` and publishes the distributable to a local directory with a manifest.
|
|
47
47
|
|
|
48
|
-
Remote publishers such as GitHub Releases are not implemented yet. Platform-specific makers,
|
|
48
|
+
Remote publishers such as GitHub Releases are not implemented yet. Platform-specific makers, Windows/Linux icon embedding, signing, and notarization are also still TODO.
|
|
49
|
+
|
|
50
|
+
Package metadata can be configured in `package.json`:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"productName": "My App",
|
|
55
|
+
"electronCli": {
|
|
56
|
+
"packagerConfig": {
|
|
57
|
+
"appBundleId": "com.example.my-app",
|
|
58
|
+
"appCategoryType": "public.app-category.developer-tools",
|
|
59
|
+
"icon": "assets/icon",
|
|
60
|
+
"extraResource": "assets/config.json"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The package command also reads JSON-shaped `config.forge.packagerConfig` and `electronPackagerConfig` entries for the same fields. JavaScript Forge config files are not evaluated.
|
|
49
67
|
|
|
50
68
|
## Install
|
|
51
69
|
|
|
@@ -103,7 +121,7 @@ The inspection and planning commands support `--json` so agents and scripts can
|
|
|
103
121
|
`plan` is designed around that workflow: it recommends stable commands and reports missing project conventions as structured data.
|
|
104
122
|
`init --dry-run --json` shows whether the CLI will write native template files or delegate to `create-electron-app`.
|
|
105
123
|
`start --dry-run --json` shows the Electron executable that will be launched.
|
|
106
|
-
`package --dry-run --json` shows the runtime
|
|
124
|
+
`package --dry-run --json` shows the runtime, app file, metadata, icon, and extra-resource copy plan.
|
|
107
125
|
`make --dry-run --json` shows the package prerequisite and ZIP artifact path.
|
|
108
126
|
`publish --dry-run --json` shows the make prerequisite, destination artifact, and manifest path.
|
|
109
127
|
|
package/package.json
CHANGED
package/src/commands/init.rs
CHANGED
|
@@ -447,8 +447,11 @@ fn write_template_file(target: &Path, relative_path: &str, contents: &str) -> Re
|
|
|
447
447
|
}
|
|
448
448
|
|
|
449
449
|
fn package_json(package_name: &str, electron_version: &str) -> Result<String> {
|
|
450
|
+
let product_name = product_name(package_name);
|
|
451
|
+
let app_bundle_id = format!("com.electron.{package_name}");
|
|
450
452
|
let package = serde_json::json!({
|
|
451
453
|
"name": package_name,
|
|
454
|
+
"productName": product_name,
|
|
452
455
|
"version": "0.1.0",
|
|
453
456
|
"private": true,
|
|
454
457
|
"description": "Minimal Electron app generated by electron-cli",
|
|
@@ -459,6 +462,11 @@ fn package_json(package_name: &str, electron_version: &str) -> Result<String> {
|
|
|
459
462
|
},
|
|
460
463
|
"devDependencies": {
|
|
461
464
|
"electron": electron_version
|
|
465
|
+
},
|
|
466
|
+
"electronCli": {
|
|
467
|
+
"packagerConfig": {
|
|
468
|
+
"appBundleId": app_bundle_id
|
|
469
|
+
}
|
|
462
470
|
}
|
|
463
471
|
});
|
|
464
472
|
|
|
@@ -639,6 +647,25 @@ fn sanitize_package_name(raw_name: &str) -> String {
|
|
|
639
647
|
}
|
|
640
648
|
}
|
|
641
649
|
|
|
650
|
+
fn product_name(package_name: &str) -> String {
|
|
651
|
+
package_name
|
|
652
|
+
.split(['-', '_', '.'])
|
|
653
|
+
.filter(|part| !part.is_empty())
|
|
654
|
+
.map(|part| {
|
|
655
|
+
let mut chars = part.chars();
|
|
656
|
+
match chars.next() {
|
|
657
|
+
Some(first) => {
|
|
658
|
+
let mut word = first.to_uppercase().collect::<String>();
|
|
659
|
+
word.push_str(chars.as_str());
|
|
660
|
+
word
|
|
661
|
+
}
|
|
662
|
+
None => String::new(),
|
|
663
|
+
}
|
|
664
|
+
})
|
|
665
|
+
.collect::<Vec<_>>()
|
|
666
|
+
.join(" ")
|
|
667
|
+
}
|
|
668
|
+
|
|
642
669
|
fn utf8_path(path: PathBuf) -> Result<Utf8PathBuf> {
|
|
643
670
|
Utf8PathBuf::from_path_buf(path).map_err(|path| {
|
|
644
671
|
anyhow::anyhow!(
|
|
@@ -756,7 +783,9 @@ mod tests {
|
|
|
756
783
|
assert!(target.join("src/renderer.js").exists());
|
|
757
784
|
assert!(target.join(".github/workflows/ci.yml").exists());
|
|
758
785
|
assert!(package_json.contains("\"name\": \"native-app\""));
|
|
786
|
+
assert!(package_json.contains("\"productName\": \"Native App\""));
|
|
759
787
|
assert!(package_json.contains("\"electron\": \"30.0.0\""));
|
|
788
|
+
assert!(package_json.contains("\"appBundleId\": \"com.electron.native-app\""));
|
|
760
789
|
assert!(config.contains("\"generator\": \"electron-cli\""));
|
|
761
790
|
|
|
762
791
|
let _ = fs::remove_dir_all(cwd);
|
|
@@ -767,6 +796,7 @@ mod tests {
|
|
|
767
796
|
assert_eq!(sanitize_package_name("Native App"), "native-app");
|
|
768
797
|
assert_eq!(sanitize_package_name("..."), "electron-app");
|
|
769
798
|
assert_eq!(sanitize_package_name("@Scope/App"), "scope-app");
|
|
799
|
+
assert_eq!(product_name("native-app"), "Native App");
|
|
770
800
|
}
|
|
771
801
|
|
|
772
802
|
fn unique_temp_dir() -> PathBuf {
|
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
|
|
@@ -845,6 +1325,135 @@ mod tests {
|
|
|
845
1325
|
let _ = fs::remove_dir_all(root);
|
|
846
1326
|
}
|
|
847
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);
|
|
1366
|
+
assert!(report
|
|
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
|
+
|
|
848
1457
|
#[test]
|
|
849
1458
|
fn missing_required_runtime_dependency_fails() {
|
|
850
1459
|
let root = unique_temp_dir("runtime-deps");
|
|
@@ -928,6 +1537,34 @@ mod tests {
|
|
|
928
1537
|
.expect("package.json should be written");
|
|
929
1538
|
}
|
|
930
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
|
+
|
|
931
1568
|
fn write_app_file(root: &Path) {
|
|
932
1569
|
fs::create_dir_all(root.join("src")).expect("src should be created");
|
|
933
1570
|
fs::write(root.join("src/main.js"), "console.log('hello');")
|
|
@@ -945,6 +1582,16 @@ mod tests {
|
|
|
945
1582
|
.expect("dependency index should be written");
|
|
946
1583
|
}
|
|
947
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
|
+
|
|
948
1595
|
fn write_fake_electron_dist(root: &Path) {
|
|
949
1596
|
let dist = root.join("node_modules/electron/dist");
|
|
950
1597
|
if current_platform() == "darwin" {
|