electron-cli 0.3.0-alpha.16 → 0.3.0-alpha.17

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.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "electron-cli"
3
- version = "0.3.0-alpha.16"
3
+ version = "0.3.0-alpha.17"
4
4
  edition = "2021"
5
5
  description = "Experimental Rust CLI for Electron project diagnostics and workflow automation"
6
6
  license = "MIT"
@@ -8,6 +8,7 @@ repository = "https://github.com/Ikana/electron-cli"
8
8
 
9
9
  [dependencies]
10
10
  anyhow = "1.0"
11
+ apple-codesign = { version = "0.29", default-features = false }
11
12
  apple-dmg = "0.5"
12
13
  cab = "0.6"
13
14
  camino = { version = "1.1", features = ["serde1"] }
package/README.md CHANGED
@@ -35,15 +35,15 @@ The Rust-native flow currently owns:
35
35
 
36
36
  - `init --template minimal`: writes a local Electron starter without Electron Forge.
37
37
  - `start`: launches the installed Electron runtime directly.
38
- - `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; it reads package metadata from `package.json`, JSON-shaped Forge config, and static Forge config files.
38
+ - `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; it reads package metadata from `package.json`, JSON-shaped Forge config, and static Forge config files, and can apply experimental Rust-native ad-hoc macOS bundle signatures.
39
39
  - `make`: runs `package` and writes distributables under `out/make/<target>/<platform>/<arch>/`; it reads JSON-shaped `config.forge.makers` / `electronCli.makers` arrays and static Forge config files when `--target` is omitted, and `--target` still forces one maker. ZIP works on all platforms, `--target dmg` writes a basic macOS disk image, `--target deb` / `--target rpm` write Linux packages, and `--target msi` writes a basic Windows Installer package.
40
40
  - `publish`: runs `make` and publishes distributables to a local directory with a manifest or to GitHub Releases; it reads JSON-shaped `config.forge.publishers` / `electronCli.publishers` arrays and static Forge config files when `--publisher` is omitted, and `--publisher` still forces one publisher.
41
41
 
42
42
  The GitHub publisher creates or reuses a release, uploads selected make artifacts, and can replace an existing asset with `--force`. It reads `GITHUB_TOKEN` or `GH_TOKEN` and can infer `OWNER/REPO` from package metadata, Forge GitHub publisher config, or `package.json` `repository`. You can also pass `--github-repo`.
43
43
 
44
- The package command recognizes macOS `packagerConfig.osxSign` and `packagerConfig.osxNotarize` options and reports the signing/notarization plan without serializing credential values. Actual Rust-native signing and notarization execution is not implemented yet.
44
+ The package command recognizes macOS `packagerConfig.osxSign` and `packagerConfig.osxNotarize` options and reports the signing/notarization plan without serializing credential values. When `osxSign` is enabled on macOS and no certificate identity is configured, or the identity is `"-"`, `package` writes an experimental Rust-native ad-hoc signature for the generated `.app` bundle. Developer ID certificate/keychain signing and notarization execution are not implemented yet.
45
45
 
46
- The DMG maker is currently a pure-Rust FAT32 image with the app bundle and an Applications entry. The MSI maker writes a compressed embedded CAB, Windows Installer database tables, and a Start Menu shortcut when the packaged executable is present. HFS+/APFS DMG layout customization, installer UI customization, Windows/Linux icon embedding, signing execution, and notarization execution are still TODO.
46
+ The DMG maker is currently a pure-Rust FAT32 image with the app bundle and an Applications entry. The MSI maker writes a compressed embedded CAB, Windows Installer database tables, and a Start Menu shortcut when the packaged executable is present. HFS+/APFS DMG layout customization, installer UI customization, Windows/Linux icon embedding, Developer ID signing execution, and notarization execution are still TODO.
47
47
 
48
48
  Package metadata can be configured in `package.json`:
49
49
 
@@ -57,7 +57,7 @@ Package metadata can be configured in `package.json`:
57
57
  "icon": "assets/icon",
58
58
  "extraResource": "assets/config.json",
59
59
  "osxSign": {
60
- "identity": "Developer ID Application: Example, Inc. (TEAMID1234)",
60
+ "identity": "-",
61
61
  "entitlements": "assets/entitlements.plist",
62
62
  "hardenedRuntime": true
63
63
  },
@@ -97,6 +97,8 @@ Package metadata can be configured in `package.json`:
97
97
  }
98
98
  ```
99
99
 
100
+ Set `identity` to a Developer ID certificate name when you want the plan to reflect Forge-style release signing, but this project will report it as not executable until Rust-native certificate/keychain signing exists. Use `identity: "-"` or omit `identity` for the current ad-hoc signing path.
101
+
100
102
  The package command also reads JSON-shaped `config.forge.packagerConfig` and `electronPackagerConfig` entries for the same fields. The make command maps JSON-shaped Forge maker names to the Rust-native targets it supports: zip, dmg, deb, rpm, and wix/msi. The publish command maps JSON-shaped publisher names to local and GitHub.
101
103
 
102
104
  Static `forge.config.js`, `forge.config.cjs`, `forge.config.mjs`, and `forge.config.ts` files are parsed in Rust when they export an object literal directly or via a local `const`/`let`/`var` identifier. Dynamic JavaScript config that calls functions, reads environment state, or computes the config at runtime is not evaluated.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electron-cli",
3
- "version": "0.3.0-alpha.16",
3
+ "version": "0.3.0-alpha.17",
4
4
  "description": "Experimental Rust CLI for Electron project diagnostics and workflow automation",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -5,6 +5,7 @@ use std::{
5
5
  };
6
6
 
7
7
  use anyhow::{bail, Context, Result};
8
+ use apple_codesign::{BundleSigner, CodeSignatureFlags, SettingsScope, SigningSettings};
8
9
  use camino::Utf8PathBuf;
9
10
  use plist::{Dictionary as PlistDictionary, Value as PlistValue};
10
11
  use serde::Serialize;
@@ -71,6 +72,8 @@ struct MacosSigningPlan {
71
72
  struct MacosSignPlan {
72
73
  configured: bool,
73
74
  enabled: bool,
75
+ will_execute: bool,
76
+ method: Option<String>,
74
77
  identity: Option<String>,
75
78
  entitlements: Vec<Utf8PathBuf>,
76
79
  entitlements_inherit: Option<Utf8PathBuf>,
@@ -363,10 +366,111 @@ pub(crate) fn execute_package(report: &PackageReport, force: bool) -> Result<()>
363
366
  &app_dir,
364
367
  &report.project,
365
368
  )?;
369
+ execute_macos_signing(report)?;
366
370
 
367
371
  Ok(())
368
372
  }
369
373
 
374
+ fn execute_macos_signing(report: &PackageReport) -> Result<()> {
375
+ if report.platform != "darwin" || !report.signing.macos.sign.will_execute {
376
+ return Ok(());
377
+ }
378
+
379
+ let bundle_dir = Path::new(report.bundle_dir.as_str());
380
+ let bundle_parent = bundle_dir
381
+ .parent()
382
+ .context("macOS bundle output has no parent directory")?;
383
+ let bundle_name = bundle_dir
384
+ .file_name()
385
+ .context("macOS bundle output has no bundle directory name")?;
386
+ let unique_suffix = std::time::SystemTime::now()
387
+ .duration_since(std::time::UNIX_EPOCH)
388
+ .context("system clock is before the Unix epoch")?
389
+ .as_nanos();
390
+ let signing_parent = bundle_parent.join(format!(
391
+ ".electron-cli-signing-{}-{unique_suffix}",
392
+ std::process::id()
393
+ ));
394
+ let signed_bundle_dir = signing_parent.join(bundle_name);
395
+
396
+ if signing_parent.exists() {
397
+ fs::remove_dir_all(&signing_parent)
398
+ .with_context(|| format!("Could not remove {}", signing_parent.display()))?;
399
+ }
400
+
401
+ let signing_result = (|| -> Result<()> {
402
+ let mut signer = BundleSigner::new_from_path(bundle_dir).with_context(|| {
403
+ format!(
404
+ "Could not prepare macOS bundle signing for {}",
405
+ bundle_dir.display()
406
+ )
407
+ })?;
408
+ signer
409
+ .collect_nested_bundles()
410
+ .context("Could not discover nested macOS bundles for signing")?;
411
+
412
+ let settings = macos_signing_settings(report)?;
413
+ signer
414
+ .write_signed_bundle(&signed_bundle_dir, &settings)
415
+ .with_context(|| {
416
+ format!(
417
+ "Could not write signed macOS bundle to {}",
418
+ signed_bundle_dir.display()
419
+ )
420
+ })?;
421
+
422
+ Ok(())
423
+ })();
424
+
425
+ if let Err(error) = signing_result {
426
+ let _ = fs::remove_dir_all(&signing_parent);
427
+ return Err(error);
428
+ }
429
+
430
+ fs::remove_dir_all(bundle_dir)
431
+ .with_context(|| format!("Could not remove {}", bundle_dir.display()))?;
432
+ fs::rename(&signed_bundle_dir, bundle_dir).with_context(|| {
433
+ format!(
434
+ "Could not move signed macOS bundle from {} to {}",
435
+ signed_bundle_dir.display(),
436
+ bundle_dir.display()
437
+ )
438
+ })?;
439
+ let _ = fs::remove_dir_all(&signing_parent);
440
+
441
+ Ok(())
442
+ }
443
+
444
+ fn macos_signing_settings(report: &PackageReport) -> Result<SigningSettings<'static>> {
445
+ let sign = &report.signing.macos.sign;
446
+ let mut settings = SigningSettings::default();
447
+ settings.set_binary_identifier(SettingsScope::Main, &report.metadata.bundle_identifier);
448
+
449
+ if sign.hardened_runtime.unwrap_or(false) {
450
+ settings.add_code_signature_flags(SettingsScope::Main, CodeSignatureFlags::RUNTIME);
451
+ }
452
+
453
+ if let Some(entitlements) = sign.entitlements.first() {
454
+ let entitlements_path = Path::new(entitlements.as_str());
455
+ let entitlements_xml = fs::read_to_string(entitlements_path).with_context(|| {
456
+ format!(
457
+ "Could not read macOS entitlements file {}",
458
+ entitlements_path.display()
459
+ )
460
+ })?;
461
+ settings
462
+ .set_entitlements_xml(SettingsScope::Main, entitlements_xml)
463
+ .with_context(|| {
464
+ format!(
465
+ "Could not parse macOS entitlements file {}",
466
+ entitlements_path.display()
467
+ )
468
+ })?;
469
+ }
470
+
471
+ Ok(settings)
472
+ }
473
+
370
474
  fn print_report(report: &PackageReport, json: bool) -> Result<()> {
371
475
  if json {
372
476
  return output::json(report);
@@ -403,6 +507,17 @@ fn print_report(report: &PackageReport, json: bool) -> Result<()> {
403
507
  if let Some(identity) = &report.signing.macos.sign.identity {
404
508
  println!(" identity: {identity}");
405
509
  }
510
+ if let Some(method) = &report.signing.macos.sign.method {
511
+ println!(" signing method: {method}");
512
+ }
513
+ println!(
514
+ " signing execution: {}",
515
+ if report.signing.macos.sign.will_execute {
516
+ "enabled"
517
+ } else {
518
+ "not available"
519
+ }
520
+ );
406
521
  println!(
407
522
  " macOS notarization: {}",
408
523
  if report.signing.macos.notarize.enabled {
@@ -720,20 +835,59 @@ fn macos_sign_plan(
720
835
  .filter(|path| !path.trim().is_empty())
721
836
  .map(|path| utf8_path(resolve_project_path(root, path)))
722
837
  .transpose()?;
838
+ if let Some(path) = &entitlements_inherit {
839
+ if !Path::new(path.as_str()).exists() {
840
+ warnings.push(format!(
841
+ "Configured macOS inherited entitlements file does not exist: {}.",
842
+ path
843
+ ));
844
+ }
845
+ }
846
+
847
+ let identity = config.identity.as_deref().map(str::trim);
848
+ let ad_hoc_identity = matches!(identity, None | Some("-"));
849
+ let will_execute = config.enabled && platform == "darwin" && ad_hoc_identity;
850
+ let method = if config.enabled && platform == "darwin" {
851
+ if ad_hoc_identity {
852
+ Some("ad-hoc".to_string())
853
+ } else {
854
+ Some("certificate-identity".to_string())
855
+ }
856
+ } else {
857
+ None
858
+ };
723
859
 
724
860
  if config.configured && platform != "darwin" {
725
861
  warnings.push(format!(
726
862
  "macOS signing is configured but ignored for target platform {platform}."
727
863
  ));
728
- } else if config.enabled {
864
+ } else if config.enabled && !will_execute {
729
865
  warnings.push(
730
- "macOS signing is configured, but Rust-native signing is not implemented yet; package output will be unsigned.".to_string(),
866
+ "macOS signing identity is configured, but Rust-native certificate/keychain signing is not implemented yet; package output will be unsigned. Use identity '-' or omit identity for experimental ad-hoc signing.".to_string(),
731
867
  );
868
+ } else if will_execute {
869
+ if config.entitlements.len() > 1 {
870
+ warnings.push(
871
+ "Rust-native ad-hoc signing applies the first macOS entitlements file only; inherited/login-helper entitlement scoping is not implemented yet.".to_string(),
872
+ );
873
+ }
874
+ if config.entitlements_inherit.is_some() {
875
+ warnings.push(
876
+ "packagerConfig.osxSign.entitlementsInherit is recognized but not applied to nested bundles by Rust-native ad-hoc signing yet.".to_string(),
877
+ );
878
+ }
879
+ if config.gatekeeper_assess.is_some() {
880
+ warnings.push(
881
+ "packagerConfig.osxSign.gatekeeperAssess is recognized but Gatekeeper assessment is not implemented yet.".to_string(),
882
+ );
883
+ }
732
884
  }
733
885
 
734
886
  Ok(MacosSignPlan {
735
887
  configured: config.configured,
736
888
  enabled: config.enabled,
889
+ will_execute,
890
+ method,
737
891
  identity: config.identity.clone(),
738
892
  entitlements,
739
893
  entitlements_inherit,
@@ -769,6 +923,23 @@ fn macos_notarize_plan(
769
923
  "macOS notarization requires packagerConfig.osxSign to be enabled first.".to_string(),
770
924
  );
771
925
  }
926
+ if config.enabled
927
+ && platform == "darwin"
928
+ && package_config.packager.osx_sign.enabled
929
+ && matches!(
930
+ package_config
931
+ .packager
932
+ .osx_sign
933
+ .identity
934
+ .as_deref()
935
+ .map(str::trim),
936
+ None | Some("-")
937
+ )
938
+ {
939
+ warnings.push(
940
+ "macOS notarization requires a Developer ID signature; Rust-native ad-hoc signing is not notarizable.".to_string(),
941
+ );
942
+ }
772
943
  if config.enabled && auth_method.is_none() {
773
944
  warnings.push(
774
945
  "macOS notarization config is missing a complete notarytool authentication set: appleId/appleIdPassword/teamId, appleApiKey/appleApiKeyId/appleApiIssuer, or keychainProfile.".to_string(),
@@ -1134,6 +1305,7 @@ fn apply_macos_metadata(report: &PackageReport) -> Result<()> {
1134
1305
  "CFBundleIdentifier",
1135
1306
  &report.metadata.bundle_identifier,
1136
1307
  );
1308
+ set_plist_string(&mut dictionary, "CFBundlePackageType", "APPL");
1137
1309
 
1138
1310
  if let Some(version) = &report.metadata.app_version {
1139
1311
  set_plist_string(&mut dictionary, "CFBundleShortVersionString", version);
@@ -1816,6 +1988,11 @@ mod tests {
1816
1988
 
1817
1989
  assert!(report.signing.macos.sign.configured);
1818
1990
  assert!(report.signing.macos.sign.enabled);
1991
+ assert!(!report.signing.macos.sign.will_execute);
1992
+ assert_eq!(
1993
+ report.signing.macos.sign.method.as_deref(),
1994
+ Some("certificate-identity")
1995
+ );
1819
1996
  assert_eq!(
1820
1997
  report.signing.macos.sign.identity.as_deref(),
1821
1998
  Some("Developer ID Application: Example, Inc. (TEAMID1234)")
@@ -1831,7 +2008,7 @@ mod tests {
1831
2008
  assert!(report
1832
2009
  .warnings
1833
2010
  .iter()
1834
- .any(|warning| warning.contains("Rust-native signing is not implemented")));
2011
+ .any(|warning| warning.contains("Rust-native certificate/keychain signing")));
1835
2012
  assert!(report
1836
2013
  .warnings
1837
2014
  .iter()
@@ -1845,6 +2022,54 @@ mod tests {
1845
2022
  let _ = fs::remove_dir_all(root);
1846
2023
  }
1847
2024
 
2025
+ #[test]
2026
+ fn plans_macos_ad_hoc_signing_execution() {
2027
+ let root = unique_temp_dir("macos-ad-hoc-signing-plan");
2028
+ write_package_json(&root);
2029
+ fs::write(
2030
+ root.join("forge.config.js"),
2031
+ r#"
2032
+ module.exports = {
2033
+ packagerConfig: {
2034
+ osxSign: {
2035
+ identity: '-',
2036
+ hardenedRuntime: true,
2037
+ },
2038
+ },
2039
+ };
2040
+ "#,
2041
+ )
2042
+ .expect("forge config should be written");
2043
+ write_app_file(&root);
2044
+ write_fake_electron_dist(&root);
2045
+
2046
+ let args = PackageArgs {
2047
+ cwd: root.clone(),
2048
+ out_dir: PathBuf::from("out"),
2049
+ name: None,
2050
+ platform: Some("darwin".to_string()),
2051
+ arch: Some("arm64".to_string()),
2052
+ force: false,
2053
+ dry_run: true,
2054
+ json: true,
2055
+ };
2056
+ let snapshot = crate::project::inspect(&root).expect("project should inspect");
2057
+ let report = build_report(snapshot, &args).expect("report should build");
2058
+
2059
+ assert!(report.signing.macos.sign.configured);
2060
+ assert!(report.signing.macos.sign.enabled);
2061
+ assert!(report.signing.macos.sign.will_execute);
2062
+ assert_eq!(report.signing.macos.sign.method.as_deref(), Some("ad-hoc"));
2063
+ assert_eq!(report.signing.macos.sign.identity.as_deref(), Some("-"));
2064
+ assert_eq!(report.signing.macos.sign.hardened_runtime, Some(true));
2065
+ assert!(!report.warnings.iter().any(|warning| {
2066
+ warning.contains("Rust-native certificate/keychain signing")
2067
+ || warning.contains("Rust-native signing is not implemented")
2068
+ }));
2069
+
2070
+ let _ = fs::remove_dir_all(root);
2071
+ }
2072
+
1848
2073
  #[test]
1849
2074
  fn warns_when_macos_notarization_is_configured_without_signing() {
1850
2075
  let root = unique_temp_dir("notarize-without-sign");
@@ -1947,6 +2172,10 @@ mod tests {
1947
2172
  plist_string(dictionary, "CFBundleIdentifier"),
1948
2173
  Some("com.example.starter")
1949
2174
  );
2175
+ assert_eq!(
2176
+ plist_string(dictionary, "CFBundlePackageType"),
2177
+ Some("APPL")
2178
+ );
1950
2179
  assert_eq!(
1951
2180
  plist_string(dictionary, "CFBundleShortVersionString"),
1952
2181
  Some("2.3.4")
@@ -1970,6 +2199,59 @@ mod tests {
1970
2199
  let _ = fs::remove_dir_all(root);
1971
2200
  }
1972
2201
 
2202
+ #[test]
2203
+ fn packages_macos_bundle_with_ad_hoc_signature() {
2204
+ if current_platform() != "darwin" {
2205
+ return;
2206
+ }
2207
+
2208
+ let root = unique_temp_dir("macos-ad-hoc-signing-execute");
2209
+ fs::write(
2210
+ root.join("package.json"),
2211
+ r#"{"name":"starter-app","version":"0.1.0","main":"src/main.js","devDependencies":{"electron":"30.0.0"},"electronCli":{"packagerConfig":{"appBundleId":"com.example.signed","osxSign":true}}}"#,
2212
+ )
2213
+ .expect("package.json should be written");
2214
+ write_app_file(&root);
2215
+ write_macho_electron_dist(&root);
2216
+
2217
+ let args = PackageArgs {
2218
+ cwd: root.clone(),
2219
+ out_dir: PathBuf::from("out"),
2220
+ name: None,
2221
+ platform: None,
2222
+ arch: None,
2223
+ force: false,
2224
+ dry_run: false,
2225
+ json: false,
2226
+ };
2227
+ let snapshot = crate::project::inspect(&root).expect("project should inspect");
2228
+ let report = build_report(snapshot, &args).expect("report should build");
2229
+
2230
+ assert!(report.signing.macos.sign.will_execute);
2231
+ assert_eq!(report.signing.macos.sign.method.as_deref(), Some("ad-hoc"));
2232
+ assert!(report.warnings.is_empty());
2233
+
2234
+ execute_package(&report, false).expect("package should succeed");
2235
+
2236
+ let bundle_dir = Path::new(report.bundle_dir.as_str());
2237
+ assert!(bundle_dir
2238
+ .join("Contents/_CodeSignature/CodeResources")
2239
+ .exists());
2240
+
2241
+ let executable = bundle_dir
2242
+ .join("Contents/MacOS")
2243
+ .join(&report.executable_name);
2244
+ let executable_data = fs::read(executable).expect("signed executable should read");
2245
+ let macho = apple_codesign::MachFile::parse(&executable_data)
2246
+ .expect("signed executable should parse as Mach-O");
2247
+ assert!(macho.iter_macho().all(|binary| binary
2248
+ .code_signature()
2249
+ .expect("code signature should parse")
2250
+ .is_some()));
2251
+
2252
+ let _ = fs::remove_dir_all(root);
2253
+ }
2254
+
1973
2255
  #[test]
1974
2256
  fn missing_required_runtime_dependency_fails() {
1975
2257
  let root = unique_temp_dir("runtime-deps");
@@ -2123,6 +2405,16 @@ mod tests {
2123
2405
  }
2124
2406
  }
2125
2407
 
2408
+ fn write_macho_electron_dist(root: &Path) {
2409
+ let app = root.join("node_modules/electron/dist/Electron.app/Contents/MacOS");
2410
+ fs::create_dir_all(&app).expect("macOS Electron app should be created");
2411
+ fs::copy(
2412
+ std::env::current_exe().expect("current test executable should resolve"),
2413
+ app.join("Electron"),
2414
+ )
2415
+ .expect("Mach-O test executable should be copied");
2416
+ }
2417
+
2126
2418
  fn unique_temp_dir(label: &str) -> PathBuf {
2127
2419
  let nanos = std::time::SystemTime::now()
2128
2420
  .duration_since(std::time::UNIX_EPOCH)