electron-cli 0.3.0-alpha.15 → 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.lock +2610 -88
- package/Cargo.toml +2 -1
- package/README.md +15 -3
- package/package.json +1 -1
- package/src/commands/package.rs +761 -0
package/src/commands/package.rs
CHANGED
|
@@ -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;
|
|
@@ -18,6 +19,7 @@ pub(crate) struct PackageReport {
|
|
|
18
19
|
app_name: String,
|
|
19
20
|
executable_name: String,
|
|
20
21
|
metadata: PackageMetadata,
|
|
22
|
+
signing: PackageSigningPlan,
|
|
21
23
|
platform: String,
|
|
22
24
|
arch: String,
|
|
23
25
|
electron_dist: Utf8PathBuf,
|
|
@@ -55,6 +57,39 @@ struct IconResource {
|
|
|
55
57
|
to: Utf8PathBuf,
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
#[derive(Clone, Debug, Serialize)]
|
|
61
|
+
struct PackageSigningPlan {
|
|
62
|
+
macos: MacosSigningPlan,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#[derive(Clone, Debug, Serialize)]
|
|
66
|
+
struct MacosSigningPlan {
|
|
67
|
+
sign: MacosSignPlan,
|
|
68
|
+
notarize: MacosNotarizePlan,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#[derive(Clone, Debug, Serialize)]
|
|
72
|
+
struct MacosSignPlan {
|
|
73
|
+
configured: bool,
|
|
74
|
+
enabled: bool,
|
|
75
|
+
will_execute: bool,
|
|
76
|
+
method: Option<String>,
|
|
77
|
+
identity: Option<String>,
|
|
78
|
+
entitlements: Vec<Utf8PathBuf>,
|
|
79
|
+
entitlements_inherit: Option<Utf8PathBuf>,
|
|
80
|
+
hardened_runtime: Option<bool>,
|
|
81
|
+
gatekeeper_assess: Option<bool>,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#[derive(Clone, Debug, Serialize)]
|
|
85
|
+
struct MacosNotarizePlan {
|
|
86
|
+
configured: bool,
|
|
87
|
+
enabled: bool,
|
|
88
|
+
auth_method: Option<String>,
|
|
89
|
+
keychain_profile: Option<String>,
|
|
90
|
+
keychain: Option<String>,
|
|
91
|
+
}
|
|
92
|
+
|
|
58
93
|
#[derive(Clone, Copy, Debug, Serialize)]
|
|
59
94
|
#[serde(rename_all = "kebab-case")]
|
|
60
95
|
enum PackageStatus {
|
|
@@ -82,6 +117,35 @@ struct PackagerConfig {
|
|
|
82
117
|
icon: Vec<String>,
|
|
83
118
|
extra_resource: Vec<String>,
|
|
84
119
|
darwin_dark_mode_support: bool,
|
|
120
|
+
osx_sign: MacosSignConfig,
|
|
121
|
+
osx_notarize: MacosNotarizeConfig,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#[derive(Clone, Debug, Default)]
|
|
125
|
+
struct MacosSignConfig {
|
|
126
|
+
configured: bool,
|
|
127
|
+
enabled: bool,
|
|
128
|
+
invalid_type: bool,
|
|
129
|
+
identity: Option<String>,
|
|
130
|
+
entitlements: Vec<String>,
|
|
131
|
+
entitlements_inherit: Option<String>,
|
|
132
|
+
hardened_runtime: Option<bool>,
|
|
133
|
+
gatekeeper_assess: Option<bool>,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#[derive(Clone, Debug, Default)]
|
|
137
|
+
struct MacosNotarizeConfig {
|
|
138
|
+
configured: bool,
|
|
139
|
+
enabled: bool,
|
|
140
|
+
invalid_type: bool,
|
|
141
|
+
apple_id_set: bool,
|
|
142
|
+
apple_id_password_set: bool,
|
|
143
|
+
team_id_set: bool,
|
|
144
|
+
apple_api_key: Option<String>,
|
|
145
|
+
apple_api_key_id_set: bool,
|
|
146
|
+
apple_api_issuer_set: bool,
|
|
147
|
+
keychain_profile: Option<String>,
|
|
148
|
+
keychain: Option<String>,
|
|
85
149
|
}
|
|
86
150
|
|
|
87
151
|
pub fn run(args: PackageArgs) -> Result<()> {
|
|
@@ -133,6 +197,7 @@ pub(crate) fn build_report(snapshot: ProjectSnapshot, args: &PackageArgs) -> Res
|
|
|
133
197
|
&app_resources_dir,
|
|
134
198
|
&platform,
|
|
135
199
|
)?;
|
|
200
|
+
let (signing, signing_warnings) = package_signing(root, &package_config, &platform)?;
|
|
136
201
|
|
|
137
202
|
let mut warnings = package_config.warnings.clone();
|
|
138
203
|
if snapshot.package_json.is_none() {
|
|
@@ -170,6 +235,7 @@ pub(crate) fn build_report(snapshot: ProjectSnapshot, args: &PackageArgs) -> Res
|
|
|
170
235
|
|
|
171
236
|
warnings.extend(runtime_dependency_warnings(root, &snapshot));
|
|
172
237
|
warnings.extend(metadata_warnings);
|
|
238
|
+
warnings.extend(signing_warnings);
|
|
173
239
|
|
|
174
240
|
let create_dirs = vec![package_root.clone(), app_resources_dir.clone()];
|
|
175
241
|
let mut copy_steps = vec![
|
|
@@ -200,6 +266,7 @@ pub(crate) fn build_report(snapshot: ProjectSnapshot, args: &PackageArgs) -> Res
|
|
|
200
266
|
app_name,
|
|
201
267
|
executable_name,
|
|
202
268
|
metadata,
|
|
269
|
+
signing,
|
|
203
270
|
platform,
|
|
204
271
|
arch,
|
|
205
272
|
electron_dist: utf8_path(electron_dist)?,
|
|
@@ -299,10 +366,111 @@ pub(crate) fn execute_package(report: &PackageReport, force: bool) -> Result<()>
|
|
|
299
366
|
&app_dir,
|
|
300
367
|
&report.project,
|
|
301
368
|
)?;
|
|
369
|
+
execute_macos_signing(report)?;
|
|
302
370
|
|
|
303
371
|
Ok(())
|
|
304
372
|
}
|
|
305
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
|
+
|
|
306
474
|
fn print_report(report: &PackageReport, json: bool) -> Result<()> {
|
|
307
475
|
if json {
|
|
308
476
|
return output::json(report);
|
|
@@ -325,6 +493,44 @@ fn print_report(report: &PackageReport, json: bool) -> Result<()> {
|
|
|
325
493
|
println!(" target: {} {}", report.platform, report.arch);
|
|
326
494
|
println!(" status: {}", report.status.as_str());
|
|
327
495
|
|
|
496
|
+
if report.signing.macos.sign.configured || report.signing.macos.notarize.configured {
|
|
497
|
+
println!();
|
|
498
|
+
println!("Signing");
|
|
499
|
+
println!(
|
|
500
|
+
" macOS signing: {}",
|
|
501
|
+
if report.signing.macos.sign.enabled {
|
|
502
|
+
"configured"
|
|
503
|
+
} else {
|
|
504
|
+
"disabled"
|
|
505
|
+
}
|
|
506
|
+
);
|
|
507
|
+
if let Some(identity) = &report.signing.macos.sign.identity {
|
|
508
|
+
println!(" identity: {identity}");
|
|
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
|
+
);
|
|
521
|
+
println!(
|
|
522
|
+
" macOS notarization: {}",
|
|
523
|
+
if report.signing.macos.notarize.enabled {
|
|
524
|
+
"configured"
|
|
525
|
+
} else {
|
|
526
|
+
"disabled"
|
|
527
|
+
}
|
|
528
|
+
);
|
|
529
|
+
if let Some(method) = &report.signing.macos.notarize.auth_method {
|
|
530
|
+
println!(" notarization auth: {method}");
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
328
534
|
println!();
|
|
329
535
|
println!("Output");
|
|
330
536
|
println!(" {}", report.bundle_dir);
|
|
@@ -400,6 +606,114 @@ fn parse_packager_config(value: &JsonValue) -> PackagerConfig {
|
|
|
400
606
|
.get("darwinDarkModeSupport")
|
|
401
607
|
.and_then(JsonValue::as_bool)
|
|
402
608
|
.unwrap_or(false),
|
|
609
|
+
osx_sign: parse_macos_sign_config(value.get("osxSign")),
|
|
610
|
+
osx_notarize: parse_macos_notarize_config(value.get("osxNotarize")),
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
fn parse_macos_sign_config(value: Option<&JsonValue>) -> MacosSignConfig {
|
|
615
|
+
match value {
|
|
616
|
+
None => MacosSignConfig::default(),
|
|
617
|
+
Some(JsonValue::Bool(false)) => MacosSignConfig {
|
|
618
|
+
configured: true,
|
|
619
|
+
enabled: false,
|
|
620
|
+
..MacosSignConfig::default()
|
|
621
|
+
},
|
|
622
|
+
Some(JsonValue::Bool(true)) => MacosSignConfig {
|
|
623
|
+
configured: true,
|
|
624
|
+
enabled: true,
|
|
625
|
+
..MacosSignConfig::default()
|
|
626
|
+
},
|
|
627
|
+
Some(JsonValue::Object(object)) => {
|
|
628
|
+
let entitlements = [
|
|
629
|
+
"entitlements",
|
|
630
|
+
"entitlementsInherit",
|
|
631
|
+
"entitlementsLoginHelper",
|
|
632
|
+
]
|
|
633
|
+
.iter()
|
|
634
|
+
.filter_map(|key| {
|
|
635
|
+
object
|
|
636
|
+
.get(*key)
|
|
637
|
+
.and_then(JsonValue::as_str)
|
|
638
|
+
.map(ToOwned::to_owned)
|
|
639
|
+
})
|
|
640
|
+
.collect();
|
|
641
|
+
|
|
642
|
+
MacosSignConfig {
|
|
643
|
+
configured: true,
|
|
644
|
+
enabled: true,
|
|
645
|
+
invalid_type: false,
|
|
646
|
+
identity: object
|
|
647
|
+
.get("identity")
|
|
648
|
+
.or_else(|| object.get("identityName"))
|
|
649
|
+
.and_then(JsonValue::as_str)
|
|
650
|
+
.map(ToOwned::to_owned),
|
|
651
|
+
entitlements,
|
|
652
|
+
entitlements_inherit: object
|
|
653
|
+
.get("entitlementsInherit")
|
|
654
|
+
.and_then(JsonValue::as_str)
|
|
655
|
+
.map(ToOwned::to_owned),
|
|
656
|
+
hardened_runtime: object.get("hardenedRuntime").and_then(JsonValue::as_bool),
|
|
657
|
+
gatekeeper_assess: object.get("gatekeeperAssess").and_then(JsonValue::as_bool),
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
Some(_) => MacosSignConfig {
|
|
661
|
+
configured: true,
|
|
662
|
+
invalid_type: true,
|
|
663
|
+
..MacosSignConfig::default()
|
|
664
|
+
},
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
fn parse_macos_notarize_config(value: Option<&JsonValue>) -> MacosNotarizeConfig {
|
|
669
|
+
match value {
|
|
670
|
+
None => MacosNotarizeConfig::default(),
|
|
671
|
+
Some(JsonValue::Bool(false)) => MacosNotarizeConfig {
|
|
672
|
+
configured: true,
|
|
673
|
+
enabled: false,
|
|
674
|
+
..MacosNotarizeConfig::default()
|
|
675
|
+
},
|
|
676
|
+
Some(JsonValue::Bool(true)) => MacosNotarizeConfig {
|
|
677
|
+
configured: true,
|
|
678
|
+
enabled: true,
|
|
679
|
+
..MacosNotarizeConfig::default()
|
|
680
|
+
},
|
|
681
|
+
Some(JsonValue::Object(object)) => MacosNotarizeConfig {
|
|
682
|
+
configured: true,
|
|
683
|
+
enabled: true,
|
|
684
|
+
invalid_type: false,
|
|
685
|
+
apple_id_set: object.get("appleId").and_then(JsonValue::as_str).is_some(),
|
|
686
|
+
apple_id_password_set: object
|
|
687
|
+
.get("appleIdPassword")
|
|
688
|
+
.and_then(JsonValue::as_str)
|
|
689
|
+
.is_some(),
|
|
690
|
+
team_id_set: object.get("teamId").and_then(JsonValue::as_str).is_some(),
|
|
691
|
+
apple_api_key: object
|
|
692
|
+
.get("appleApiKey")
|
|
693
|
+
.and_then(JsonValue::as_str)
|
|
694
|
+
.map(ToOwned::to_owned),
|
|
695
|
+
apple_api_key_id_set: object
|
|
696
|
+
.get("appleApiKeyId")
|
|
697
|
+
.and_then(JsonValue::as_str)
|
|
698
|
+
.is_some(),
|
|
699
|
+
apple_api_issuer_set: object
|
|
700
|
+
.get("appleApiIssuer")
|
|
701
|
+
.and_then(JsonValue::as_str)
|
|
702
|
+
.is_some(),
|
|
703
|
+
keychain_profile: object
|
|
704
|
+
.get("keychainProfile")
|
|
705
|
+
.and_then(JsonValue::as_str)
|
|
706
|
+
.map(ToOwned::to_owned),
|
|
707
|
+
keychain: object
|
|
708
|
+
.get("keychain")
|
|
709
|
+
.and_then(JsonValue::as_str)
|
|
710
|
+
.map(ToOwned::to_owned),
|
|
711
|
+
},
|
|
712
|
+
Some(_) => MacosNotarizeConfig {
|
|
713
|
+
configured: true,
|
|
714
|
+
invalid_type: true,
|
|
715
|
+
..MacosNotarizeConfig::default()
|
|
716
|
+
},
|
|
403
717
|
}
|
|
404
718
|
}
|
|
405
719
|
|
|
@@ -473,6 +787,202 @@ fn package_metadata(
|
|
|
473
787
|
))
|
|
474
788
|
}
|
|
475
789
|
|
|
790
|
+
fn package_signing(
|
|
791
|
+
root: &Path,
|
|
792
|
+
config: &PackageJsonConfig,
|
|
793
|
+
platform: &str,
|
|
794
|
+
) -> Result<(PackageSigningPlan, Vec<String>)> {
|
|
795
|
+
let mut warnings = Vec::new();
|
|
796
|
+
let sign = macos_sign_plan(root, &config.packager.osx_sign, platform, &mut warnings)?;
|
|
797
|
+
let notarize = macos_notarize_plan(root, config, platform, &mut warnings);
|
|
798
|
+
|
|
799
|
+
Ok((
|
|
800
|
+
PackageSigningPlan {
|
|
801
|
+
macos: MacosSigningPlan { sign, notarize },
|
|
802
|
+
},
|
|
803
|
+
warnings,
|
|
804
|
+
))
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
fn macos_sign_plan(
|
|
808
|
+
root: &Path,
|
|
809
|
+
config: &MacosSignConfig,
|
|
810
|
+
platform: &str,
|
|
811
|
+
warnings: &mut Vec<String>,
|
|
812
|
+
) -> Result<MacosSignPlan> {
|
|
813
|
+
if config.invalid_type {
|
|
814
|
+
warnings.push("packagerConfig.osxSign must be false, true, or an object.".to_string());
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
let entitlements = config
|
|
818
|
+
.entitlements
|
|
819
|
+
.iter()
|
|
820
|
+
.filter(|path| !path.trim().is_empty())
|
|
821
|
+
.map(|path| {
|
|
822
|
+
let resolved = resolve_project_path(root, path);
|
|
823
|
+
if !resolved.exists() {
|
|
824
|
+
warnings.push(format!(
|
|
825
|
+
"Configured macOS entitlements file does not exist: {}.",
|
|
826
|
+
resolved.display()
|
|
827
|
+
));
|
|
828
|
+
}
|
|
829
|
+
utf8_path(resolved)
|
|
830
|
+
})
|
|
831
|
+
.collect::<Result<Vec<_>>>()?;
|
|
832
|
+
let entitlements_inherit = config
|
|
833
|
+
.entitlements_inherit
|
|
834
|
+
.as_deref()
|
|
835
|
+
.filter(|path| !path.trim().is_empty())
|
|
836
|
+
.map(|path| utf8_path(resolve_project_path(root, path)))
|
|
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
|
+
};
|
|
859
|
+
|
|
860
|
+
if config.configured && platform != "darwin" {
|
|
861
|
+
warnings.push(format!(
|
|
862
|
+
"macOS signing is configured but ignored for target platform {platform}."
|
|
863
|
+
));
|
|
864
|
+
} else if config.enabled && !will_execute {
|
|
865
|
+
warnings.push(
|
|
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(),
|
|
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
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
Ok(MacosSignPlan {
|
|
887
|
+
configured: config.configured,
|
|
888
|
+
enabled: config.enabled,
|
|
889
|
+
will_execute,
|
|
890
|
+
method,
|
|
891
|
+
identity: config.identity.clone(),
|
|
892
|
+
entitlements,
|
|
893
|
+
entitlements_inherit,
|
|
894
|
+
hardened_runtime: config.hardened_runtime,
|
|
895
|
+
gatekeeper_assess: config.gatekeeper_assess,
|
|
896
|
+
})
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
fn macos_notarize_plan(
|
|
900
|
+
root: &Path,
|
|
901
|
+
package_config: &PackageJsonConfig,
|
|
902
|
+
platform: &str,
|
|
903
|
+
warnings: &mut Vec<String>,
|
|
904
|
+
) -> MacosNotarizePlan {
|
|
905
|
+
let config = &package_config.packager.osx_notarize;
|
|
906
|
+
if config.invalid_type {
|
|
907
|
+
warnings.push("packagerConfig.osxNotarize must be false, true, or an object.".to_string());
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
let auth_method = macos_notarize_auth_method(config);
|
|
911
|
+
if config.configured && platform != "darwin" {
|
|
912
|
+
warnings.push(format!(
|
|
913
|
+
"macOS notarization is configured but ignored for target platform {platform}."
|
|
914
|
+
));
|
|
915
|
+
} else if config.enabled {
|
|
916
|
+
warnings.push(
|
|
917
|
+
"macOS notarization is configured, but Rust-native notarization is not implemented yet.".to_string(),
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if config.enabled && !package_config.packager.osx_sign.enabled {
|
|
922
|
+
warnings.push(
|
|
923
|
+
"macOS notarization requires packagerConfig.osxSign to be enabled first.".to_string(),
|
|
924
|
+
);
|
|
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
|
+
}
|
|
943
|
+
if config.enabled && auth_method.is_none() {
|
|
944
|
+
warnings.push(
|
|
945
|
+
"macOS notarization config is missing a complete notarytool authentication set: appleId/appleIdPassword/teamId, appleApiKey/appleApiKeyId/appleApiIssuer, or keychainProfile.".to_string(),
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
if let Some(api_key) = &config.apple_api_key {
|
|
949
|
+
let path = resolve_project_path(root, api_key);
|
|
950
|
+
if !path.exists() {
|
|
951
|
+
warnings.push(format!(
|
|
952
|
+
"Configured Apple API key file does not exist: {}.",
|
|
953
|
+
path.display()
|
|
954
|
+
));
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
MacosNotarizePlan {
|
|
959
|
+
configured: config.configured,
|
|
960
|
+
enabled: config.enabled,
|
|
961
|
+
auth_method,
|
|
962
|
+
keychain_profile: config.keychain_profile.clone(),
|
|
963
|
+
keychain: config.keychain.clone(),
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
fn macos_notarize_auth_method(config: &MacosNotarizeConfig) -> Option<String> {
|
|
968
|
+
if config
|
|
969
|
+
.keychain_profile
|
|
970
|
+
.as_deref()
|
|
971
|
+
.is_some_and(|value| !value.trim().is_empty())
|
|
972
|
+
{
|
|
973
|
+
Some("keychain-profile".to_string())
|
|
974
|
+
} else if config.apple_api_key.is_some()
|
|
975
|
+
&& config.apple_api_key_id_set
|
|
976
|
+
&& config.apple_api_issuer_set
|
|
977
|
+
{
|
|
978
|
+
Some("app-store-connect-api-key".to_string())
|
|
979
|
+
} else if config.apple_id_set && config.apple_id_password_set && config.team_id_set {
|
|
980
|
+
Some("apple-id".to_string())
|
|
981
|
+
} else {
|
|
982
|
+
None
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
476
986
|
fn resolve_icon_resource(
|
|
477
987
|
root: &Path,
|
|
478
988
|
configured_icons: &[String],
|
|
@@ -795,6 +1305,7 @@ fn apply_macos_metadata(report: &PackageReport) -> Result<()> {
|
|
|
795
1305
|
"CFBundleIdentifier",
|
|
796
1306
|
&report.metadata.bundle_identifier,
|
|
797
1307
|
);
|
|
1308
|
+
set_plist_string(&mut dictionary, "CFBundlePackageType", "APPL");
|
|
798
1309
|
|
|
799
1310
|
if let Some(version) = &report.metadata.app_version {
|
|
800
1311
|
set_plist_string(&mut dictionary, "CFBundleShortVersionString", version);
|
|
@@ -1158,6 +1669,12 @@ impl PackagerConfig {
|
|
|
1158
1669
|
}
|
|
1159
1670
|
self.darwin_dark_mode_support =
|
|
1160
1671
|
other.darwin_dark_mode_support || self.darwin_dark_mode_support;
|
|
1672
|
+
if other.osx_sign.configured {
|
|
1673
|
+
self.osx_sign = other.osx_sign;
|
|
1674
|
+
}
|
|
1675
|
+
if other.osx_notarize.configured {
|
|
1676
|
+
self.osx_notarize = other.osx_notarize;
|
|
1677
|
+
}
|
|
1161
1678
|
}
|
|
1162
1679
|
}
|
|
1163
1680
|
|
|
@@ -1423,6 +1940,183 @@ mod tests {
|
|
|
1423
1940
|
let _ = fs::remove_dir_all(root);
|
|
1424
1941
|
}
|
|
1425
1942
|
|
|
1943
|
+
#[test]
|
|
1944
|
+
fn plans_macos_signing_and_notarization_without_serializing_secrets() {
|
|
1945
|
+
let root = unique_temp_dir("macos-signing-plan");
|
|
1946
|
+
write_package_json(&root);
|
|
1947
|
+
fs::write(root.join("entitlements.plist"), "<plist></plist>")
|
|
1948
|
+
.expect("entitlements should be written");
|
|
1949
|
+
fs::write(root.join("AuthKey_TEST.p8"), "secret api key")
|
|
1950
|
+
.expect("api key should be written");
|
|
1951
|
+
fs::write(
|
|
1952
|
+
root.join("forge.config.js"),
|
|
1953
|
+
r#"
|
|
1954
|
+
module.exports = {
|
|
1955
|
+
packagerConfig: {
|
|
1956
|
+
osxSign: {
|
|
1957
|
+
identity: 'Developer ID Application: Example, Inc. (TEAMID1234)',
|
|
1958
|
+
entitlements: 'entitlements.plist',
|
|
1959
|
+
entitlementsInherit: 'entitlements.plist',
|
|
1960
|
+
hardenedRuntime: true,
|
|
1961
|
+
gatekeeperAssess: false,
|
|
1962
|
+
},
|
|
1963
|
+
osxNotarize: {
|
|
1964
|
+
appleApiKey: 'AuthKey_TEST.p8',
|
|
1965
|
+
appleApiKeyId: 'SECRET_KEY_ID',
|
|
1966
|
+
appleApiIssuer: 'SECRET_ISSUER_ID',
|
|
1967
|
+
},
|
|
1968
|
+
},
|
|
1969
|
+
};
|
|
1970
|
+
"#,
|
|
1971
|
+
)
|
|
1972
|
+
.expect("forge config should be written");
|
|
1973
|
+
write_app_file(&root);
|
|
1974
|
+
write_fake_electron_dist(&root);
|
|
1975
|
+
|
|
1976
|
+
let args = PackageArgs {
|
|
1977
|
+
cwd: root.clone(),
|
|
1978
|
+
out_dir: PathBuf::from("out"),
|
|
1979
|
+
name: None,
|
|
1980
|
+
platform: Some("darwin".to_string()),
|
|
1981
|
+
arch: Some("arm64".to_string()),
|
|
1982
|
+
force: false,
|
|
1983
|
+
dry_run: true,
|
|
1984
|
+
json: true,
|
|
1985
|
+
};
|
|
1986
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
1987
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
1988
|
+
|
|
1989
|
+
assert!(report.signing.macos.sign.configured);
|
|
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
|
+
);
|
|
1996
|
+
assert_eq!(
|
|
1997
|
+
report.signing.macos.sign.identity.as_deref(),
|
|
1998
|
+
Some("Developer ID Application: Example, Inc. (TEAMID1234)")
|
|
1999
|
+
);
|
|
2000
|
+
assert_eq!(report.signing.macos.sign.hardened_runtime, Some(true));
|
|
2001
|
+
assert_eq!(report.signing.macos.sign.gatekeeper_assess, Some(false));
|
|
2002
|
+
assert_eq!(report.signing.macos.sign.entitlements.len(), 2);
|
|
2003
|
+
assert!(report.signing.macos.notarize.configured);
|
|
2004
|
+
assert_eq!(
|
|
2005
|
+
report.signing.macos.notarize.auth_method.as_deref(),
|
|
2006
|
+
Some("app-store-connect-api-key")
|
|
2007
|
+
);
|
|
2008
|
+
assert!(report
|
|
2009
|
+
.warnings
|
|
2010
|
+
.iter()
|
|
2011
|
+
.any(|warning| warning.contains("Rust-native certificate/keychain signing")));
|
|
2012
|
+
assert!(report
|
|
2013
|
+
.warnings
|
|
2014
|
+
.iter()
|
|
2015
|
+
.any(|warning| warning.contains("Rust-native notarization is not implemented")));
|
|
2016
|
+
|
|
2017
|
+
let json = serde_json::to_string(&report).expect("report should serialize");
|
|
2018
|
+
assert!(!json.contains("SECRET_KEY_ID"));
|
|
2019
|
+
assert!(!json.contains("SECRET_ISSUER_ID"));
|
|
2020
|
+
assert!(!json.contains("secret api key"));
|
|
2021
|
+
|
|
2022
|
+
let _ = fs::remove_dir_all(root);
|
|
2023
|
+
}
|
|
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
|
+
|
|
2073
|
+
#[test]
|
|
2074
|
+
fn warns_when_macos_notarization_is_configured_without_signing() {
|
|
2075
|
+
let root = unique_temp_dir("notarize-without-sign");
|
|
2076
|
+
write_package_json(&root);
|
|
2077
|
+
fs::write(
|
|
2078
|
+
root.join("forge.config.js"),
|
|
2079
|
+
r#"
|
|
2080
|
+
module.exports = {
|
|
2081
|
+
packagerConfig: {
|
|
2082
|
+
osxSign: false,
|
|
2083
|
+
osxNotarize: {
|
|
2084
|
+
keychainProfile: 'notary-profile',
|
|
2085
|
+
},
|
|
2086
|
+
},
|
|
2087
|
+
};
|
|
2088
|
+
"#,
|
|
2089
|
+
)
|
|
2090
|
+
.expect("forge config should be written");
|
|
2091
|
+
write_app_file(&root);
|
|
2092
|
+
write_fake_electron_dist(&root);
|
|
2093
|
+
|
|
2094
|
+
let args = PackageArgs {
|
|
2095
|
+
cwd: root.clone(),
|
|
2096
|
+
out_dir: PathBuf::from("out"),
|
|
2097
|
+
name: None,
|
|
2098
|
+
platform: Some("darwin".to_string()),
|
|
2099
|
+
arch: Some("arm64".to_string()),
|
|
2100
|
+
force: false,
|
|
2101
|
+
dry_run: true,
|
|
2102
|
+
json: true,
|
|
2103
|
+
};
|
|
2104
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
2105
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
2106
|
+
|
|
2107
|
+
assert!(report.signing.macos.sign.configured);
|
|
2108
|
+
assert!(!report.signing.macos.sign.enabled);
|
|
2109
|
+
assert_eq!(
|
|
2110
|
+
report.signing.macos.notarize.auth_method.as_deref(),
|
|
2111
|
+
Some("keychain-profile")
|
|
2112
|
+
);
|
|
2113
|
+
assert!(report.warnings.iter().any(|warning| {
|
|
2114
|
+
warning.contains("macOS notarization requires packagerConfig.osxSign")
|
|
2115
|
+
}));
|
|
2116
|
+
|
|
2117
|
+
let _ = fs::remove_dir_all(root);
|
|
2118
|
+
}
|
|
2119
|
+
|
|
1426
2120
|
#[test]
|
|
1427
2121
|
fn packages_macos_info_plist_metadata() {
|
|
1428
2122
|
if current_platform() != "darwin" {
|
|
@@ -1478,6 +2172,10 @@ mod tests {
|
|
|
1478
2172
|
plist_string(dictionary, "CFBundleIdentifier"),
|
|
1479
2173
|
Some("com.example.starter")
|
|
1480
2174
|
);
|
|
2175
|
+
assert_eq!(
|
|
2176
|
+
plist_string(dictionary, "CFBundlePackageType"),
|
|
2177
|
+
Some("APPL")
|
|
2178
|
+
);
|
|
1481
2179
|
assert_eq!(
|
|
1482
2180
|
plist_string(dictionary, "CFBundleShortVersionString"),
|
|
1483
2181
|
Some("2.3.4")
|
|
@@ -1501,6 +2199,59 @@ mod tests {
|
|
|
1501
2199
|
let _ = fs::remove_dir_all(root);
|
|
1502
2200
|
}
|
|
1503
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
|
+
|
|
1504
2255
|
#[test]
|
|
1505
2256
|
fn missing_required_runtime_dependency_fails() {
|
|
1506
2257
|
let root = unique_temp_dir("runtime-deps");
|
|
@@ -1654,6 +2405,16 @@ mod tests {
|
|
|
1654
2405
|
}
|
|
1655
2406
|
}
|
|
1656
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
|
+
|
|
1657
2418
|
fn unique_temp_dir(label: &str) -> PathBuf {
|
|
1658
2419
|
let nanos = std::time::SystemTime::now()
|
|
1659
2420
|
.duration_since(std::time::UNIX_EPOCH)
|