electron-cli 0.3.0-alpha.2 → 0.3.0-alpha.20
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 +5380 -101
- package/Cargo.toml +17 -1
- package/README.md +103 -12
- package/package.json +2 -1
- package/src/cli.rs +226 -4
- package/src/commands/init.rs +443 -27
- package/src/commands/make.rs +3076 -0
- package/src/commands/mod.rs +4 -0
- package/src/commands/package.rs +3238 -0
- package/src/commands/plan.rs +65 -5
- package/src/commands/publish.rs +1832 -0
- package/src/commands/start.rs +287 -0
- package/src/forge_config.rs +547 -0
- package/src/main.rs +5 -0
- package/src/project.rs +52 -1
- package/templates/minimal/gitignore +5 -0
- package/templates/minimal/src/index.html +82 -0
- package/templates/minimal/src/main.js +33 -0
- package/templates/minimal/src/preload.js +6 -0
- package/templates/minimal/src/renderer.js +5 -0
|
@@ -0,0 +1,3238 @@
|
|
|
1
|
+
use std::{
|
|
2
|
+
collections::{BTreeMap, BTreeSet, VecDeque},
|
|
3
|
+
fs,
|
|
4
|
+
path::{Path, PathBuf},
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
use anyhow::{bail, Context, Result};
|
|
8
|
+
use app_store_connect::UnifiedApiKey;
|
|
9
|
+
use apple_codesign::{
|
|
10
|
+
cryptography::{parse_pfx_data, PrivateKey},
|
|
11
|
+
stapling::Stapler,
|
|
12
|
+
BundleSigner, CodeSignatureFlags, NotarizationUpload, Notarizer, SettingsScope,
|
|
13
|
+
SigningSettings,
|
|
14
|
+
};
|
|
15
|
+
use camino::Utf8PathBuf;
|
|
16
|
+
use plist::{Dictionary as PlistDictionary, Value as PlistValue};
|
|
17
|
+
use serde::Serialize;
|
|
18
|
+
use serde_json::Value as JsonValue;
|
|
19
|
+
|
|
20
|
+
use crate::{cli::PackageArgs, output, project::ProjectSnapshot};
|
|
21
|
+
|
|
22
|
+
const APPLE_TIMESTAMP_URL: &str = "http://timestamp.apple.com/ts01";
|
|
23
|
+
const MACOS_NOTARIZATION_WAIT_TIMEOUT_SECONDS: u64 = 600;
|
|
24
|
+
|
|
25
|
+
#[derive(Clone, Debug, Serialize)]
|
|
26
|
+
pub(crate) struct PackageReport {
|
|
27
|
+
project: ProjectSnapshot,
|
|
28
|
+
app_name: String,
|
|
29
|
+
executable_name: String,
|
|
30
|
+
metadata: PackageMetadata,
|
|
31
|
+
signing: PackageSigningPlan,
|
|
32
|
+
platform: String,
|
|
33
|
+
arch: String,
|
|
34
|
+
electron_dist: Utf8PathBuf,
|
|
35
|
+
output_dir: Utf8PathBuf,
|
|
36
|
+
bundle_dir: Utf8PathBuf,
|
|
37
|
+
app_resources_dir: Utf8PathBuf,
|
|
38
|
+
dry_run: bool,
|
|
39
|
+
status: PackageStatus,
|
|
40
|
+
create_dirs: Vec<Utf8PathBuf>,
|
|
41
|
+
copy_steps: Vec<CopyStep>,
|
|
42
|
+
warnings: Vec<String>,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[derive(Clone, Debug, Serialize)]
|
|
46
|
+
struct CopyStep {
|
|
47
|
+
from: Utf8PathBuf,
|
|
48
|
+
to: Utf8PathBuf,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#[derive(Clone, Debug, Serialize)]
|
|
52
|
+
struct PackageMetadata {
|
|
53
|
+
bundle_identifier: String,
|
|
54
|
+
app_version: Option<String>,
|
|
55
|
+
build_version: Option<String>,
|
|
56
|
+
app_category_type: Option<String>,
|
|
57
|
+
app_copyright: Option<String>,
|
|
58
|
+
icon: Option<IconResource>,
|
|
59
|
+
extra_resources: Vec<CopyStep>,
|
|
60
|
+
darwin_dark_mode_support: bool,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[derive(Clone, Debug, Serialize)]
|
|
64
|
+
struct IconResource {
|
|
65
|
+
from: Utf8PathBuf,
|
|
66
|
+
to: Utf8PathBuf,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[derive(Clone, Debug, Serialize)]
|
|
70
|
+
struct PackageSigningPlan {
|
|
71
|
+
macos: MacosSigningPlan,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#[derive(Clone, Debug, Serialize)]
|
|
75
|
+
struct MacosSigningPlan {
|
|
76
|
+
sign: MacosSignPlan,
|
|
77
|
+
notarize: MacosNotarizePlan,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#[derive(Clone, Debug, Serialize)]
|
|
81
|
+
struct MacosSignPlan {
|
|
82
|
+
configured: bool,
|
|
83
|
+
enabled: bool,
|
|
84
|
+
will_execute: bool,
|
|
85
|
+
method: Option<String>,
|
|
86
|
+
identity: Option<String>,
|
|
87
|
+
p12_file: Option<Utf8PathBuf>,
|
|
88
|
+
p12_password_source: Option<String>,
|
|
89
|
+
p12_password_env: Option<String>,
|
|
90
|
+
p12_password_file: Option<Utf8PathBuf>,
|
|
91
|
+
#[serde(skip)]
|
|
92
|
+
p12_password: RedactedSecret,
|
|
93
|
+
timestamp_url: Option<String>,
|
|
94
|
+
for_notarization: bool,
|
|
95
|
+
entitlements: Vec<Utf8PathBuf>,
|
|
96
|
+
entitlements_inherit: Option<Utf8PathBuf>,
|
|
97
|
+
hardened_runtime: Option<bool>,
|
|
98
|
+
gatekeeper_assess: Option<bool>,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#[derive(Clone, Debug, Serialize)]
|
|
102
|
+
struct MacosNotarizePlan {
|
|
103
|
+
configured: bool,
|
|
104
|
+
enabled: bool,
|
|
105
|
+
will_execute: bool,
|
|
106
|
+
auth_method: Option<String>,
|
|
107
|
+
apple_api_key: Option<Utf8PathBuf>,
|
|
108
|
+
#[serde(skip)]
|
|
109
|
+
apple_api_key_id: RedactedSecret,
|
|
110
|
+
#[serde(skip)]
|
|
111
|
+
apple_api_issuer: RedactedSecret,
|
|
112
|
+
keychain_profile: Option<String>,
|
|
113
|
+
keychain: Option<String>,
|
|
114
|
+
wait: bool,
|
|
115
|
+
wait_timeout_seconds: u64,
|
|
116
|
+
staple: bool,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#[derive(Clone, Default)]
|
|
120
|
+
struct RedactedSecret(Option<String>);
|
|
121
|
+
|
|
122
|
+
impl std::fmt::Debug for RedactedSecret {
|
|
123
|
+
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
124
|
+
if self.0.is_some() {
|
|
125
|
+
formatter.write_str("<redacted>")
|
|
126
|
+
} else {
|
|
127
|
+
formatter.write_str("<unset>")
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
impl RedactedSecret {
|
|
133
|
+
fn new(value: Option<String>) -> Self {
|
|
134
|
+
Self(value)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fn as_deref(&self) -> Option<&str> {
|
|
138
|
+
self.0.as_deref()
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#[derive(Clone, Copy, Debug, Serialize)]
|
|
143
|
+
#[serde(rename_all = "kebab-case")]
|
|
144
|
+
enum PackageStatus {
|
|
145
|
+
Planned,
|
|
146
|
+
Packaged,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#[derive(Debug, Default)]
|
|
150
|
+
struct PackageJsonConfig {
|
|
151
|
+
product_name: Option<String>,
|
|
152
|
+
app_version: Option<String>,
|
|
153
|
+
packager: PackagerConfig,
|
|
154
|
+
warnings: Vec<String>,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#[derive(Debug, Default)]
|
|
158
|
+
struct PackagerConfig {
|
|
159
|
+
name: Option<String>,
|
|
160
|
+
executable_name: Option<String>,
|
|
161
|
+
app_bundle_id: Option<String>,
|
|
162
|
+
app_category_type: Option<String>,
|
|
163
|
+
app_version: Option<String>,
|
|
164
|
+
build_version: Option<String>,
|
|
165
|
+
app_copyright: Option<String>,
|
|
166
|
+
icon: Vec<String>,
|
|
167
|
+
extra_resource: Vec<String>,
|
|
168
|
+
darwin_dark_mode_support: bool,
|
|
169
|
+
osx_sign: MacosSignConfig,
|
|
170
|
+
osx_notarize: MacosNotarizeConfig,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#[derive(Clone, Debug, Default)]
|
|
174
|
+
struct MacosSignConfig {
|
|
175
|
+
configured: bool,
|
|
176
|
+
enabled: bool,
|
|
177
|
+
invalid_type: bool,
|
|
178
|
+
identity: Option<String>,
|
|
179
|
+
p12_file: Option<String>,
|
|
180
|
+
p12_password: Option<String>,
|
|
181
|
+
p12_password_env: Option<String>,
|
|
182
|
+
p12_password_file: Option<String>,
|
|
183
|
+
timestamp: Option<MacosTimestampConfig>,
|
|
184
|
+
entitlements: Vec<String>,
|
|
185
|
+
entitlements_inherit: Option<String>,
|
|
186
|
+
hardened_runtime: Option<bool>,
|
|
187
|
+
gatekeeper_assess: Option<bool>,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#[derive(Clone, Debug)]
|
|
191
|
+
enum MacosTimestampConfig {
|
|
192
|
+
Default,
|
|
193
|
+
Disabled,
|
|
194
|
+
Url(String),
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
#[derive(Clone, Debug, Default)]
|
|
198
|
+
struct MacosNotarizeConfig {
|
|
199
|
+
configured: bool,
|
|
200
|
+
enabled: bool,
|
|
201
|
+
invalid_type: bool,
|
|
202
|
+
apple_id_set: bool,
|
|
203
|
+
apple_id_password_set: bool,
|
|
204
|
+
team_id_set: bool,
|
|
205
|
+
apple_api_key: Option<String>,
|
|
206
|
+
apple_api_key_id: Option<String>,
|
|
207
|
+
apple_api_issuer: Option<String>,
|
|
208
|
+
keychain_profile: Option<String>,
|
|
209
|
+
keychain: Option<String>,
|
|
210
|
+
wait: Option<bool>,
|
|
211
|
+
wait_timeout_seconds: Option<u64>,
|
|
212
|
+
staple: Option<bool>,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
pub fn run(args: PackageArgs) -> Result<()> {
|
|
216
|
+
let snapshot = crate::project::inspect(&args.cwd)?;
|
|
217
|
+
let mut report = build_report(snapshot, &args)?;
|
|
218
|
+
|
|
219
|
+
if args.dry_run {
|
|
220
|
+
return print_report(&report, args.json);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
execute_package(&report, args.force)?;
|
|
224
|
+
report.status = PackageStatus::Packaged;
|
|
225
|
+
|
|
226
|
+
print_report(&report, args.json)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
pub(crate) fn build_report(snapshot: ProjectSnapshot, args: &PackageArgs) -> Result<PackageReport> {
|
|
230
|
+
let root = Path::new(snapshot.root.as_str());
|
|
231
|
+
let package_config = read_package_json_config(&snapshot)?;
|
|
232
|
+
let platform = args.platform.clone().unwrap_or_else(current_platform);
|
|
233
|
+
let arch = args.arch.clone().unwrap_or_else(current_arch);
|
|
234
|
+
let app_name = clean_app_name(
|
|
235
|
+
&args
|
|
236
|
+
.name
|
|
237
|
+
.clone()
|
|
238
|
+
.or_else(|| package_config.packager.name.clone())
|
|
239
|
+
.or_else(|| package_config.product_name.clone())
|
|
240
|
+
.or_else(|| snapshot.name.clone())
|
|
241
|
+
.unwrap_or_else(|| "electron-app".to_string()),
|
|
242
|
+
);
|
|
243
|
+
let executable_base = package_config
|
|
244
|
+
.packager
|
|
245
|
+
.executable_name
|
|
246
|
+
.clone()
|
|
247
|
+
.map(|name| clean_app_name(&name))
|
|
248
|
+
.unwrap_or_else(|| app_name.clone());
|
|
249
|
+
let executable_name = executable_name(&executable_base, &platform);
|
|
250
|
+
let artifact_name = sanitize_artifact_name(&app_name);
|
|
251
|
+
let output_dir = resolve_output_dir(root, &args.out_dir);
|
|
252
|
+
let package_root = output_dir.join(format!("{artifact_name}-{platform}-{arch}"));
|
|
253
|
+
let bundle_dir = bundle_dir(&package_root, &app_name, &platform);
|
|
254
|
+
let app_resources_dir = app_resources_dir(&bundle_dir, &platform);
|
|
255
|
+
let electron_dist = root.join("node_modules/electron/dist");
|
|
256
|
+
let electron_source = electron_source(&electron_dist, &platform);
|
|
257
|
+
let (metadata, metadata_warnings) = package_metadata(
|
|
258
|
+
root,
|
|
259
|
+
&package_config,
|
|
260
|
+
&artifact_name,
|
|
261
|
+
&app_resources_dir,
|
|
262
|
+
&platform,
|
|
263
|
+
)?;
|
|
264
|
+
let (signing, signing_warnings) = package_signing(root, &package_config, &platform)?;
|
|
265
|
+
|
|
266
|
+
let mut warnings = package_config.warnings.clone();
|
|
267
|
+
if snapshot.package_json.is_none() {
|
|
268
|
+
warnings.push("No package.json found.".to_string());
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if snapshot.electron_dependency.is_none() {
|
|
272
|
+
warnings.push("No electron dependency is declared in package.json.".to_string());
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if snapshot.main.is_none() {
|
|
276
|
+
warnings.push("No package.json main field found.".to_string());
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if !electron_source.exists() {
|
|
280
|
+
warnings.push(format!(
|
|
281
|
+
"Electron runtime was not found at {}.",
|
|
282
|
+
electron_source.display()
|
|
283
|
+
));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if platform != current_platform() {
|
|
287
|
+
warnings.push(format!(
|
|
288
|
+
"Cross-platform packaging is not implemented yet; this host can package {}.",
|
|
289
|
+
current_platform()
|
|
290
|
+
));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if arch != current_arch() {
|
|
294
|
+
warnings.push(format!(
|
|
295
|
+
"Cross-architecture packaging is not implemented yet; this host can package {}.",
|
|
296
|
+
current_arch()
|
|
297
|
+
));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
warnings.extend(runtime_dependency_warnings(root, &snapshot));
|
|
301
|
+
warnings.extend(metadata_warnings);
|
|
302
|
+
warnings.extend(signing_warnings);
|
|
303
|
+
|
|
304
|
+
let create_dirs = vec![package_root.clone(), app_resources_dir.clone()];
|
|
305
|
+
let mut copy_steps = vec![
|
|
306
|
+
(electron_source, bundle_dir.clone()),
|
|
307
|
+
(root.to_path_buf(), app_resources_dir.join("app")),
|
|
308
|
+
];
|
|
309
|
+
if has_runtime_dependencies(&snapshot) {
|
|
310
|
+
copy_steps.push((
|
|
311
|
+
root.join("node_modules"),
|
|
312
|
+
app_resources_dir.join("app/node_modules"),
|
|
313
|
+
));
|
|
314
|
+
}
|
|
315
|
+
if let Some(icon) = &metadata.icon {
|
|
316
|
+
copy_steps.push((
|
|
317
|
+
Path::new(icon.from.as_str()).to_path_buf(),
|
|
318
|
+
Path::new(icon.to.as_str()).to_path_buf(),
|
|
319
|
+
));
|
|
320
|
+
}
|
|
321
|
+
for resource in &metadata.extra_resources {
|
|
322
|
+
copy_steps.push((
|
|
323
|
+
Path::new(resource.from.as_str()).to_path_buf(),
|
|
324
|
+
Path::new(resource.to.as_str()).to_path_buf(),
|
|
325
|
+
));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
Ok(PackageReport {
|
|
329
|
+
project: snapshot,
|
|
330
|
+
app_name,
|
|
331
|
+
executable_name,
|
|
332
|
+
metadata,
|
|
333
|
+
signing,
|
|
334
|
+
platform,
|
|
335
|
+
arch,
|
|
336
|
+
electron_dist: utf8_path(electron_dist)?,
|
|
337
|
+
output_dir: utf8_path(output_dir)?,
|
|
338
|
+
bundle_dir: utf8_path(bundle_dir)?,
|
|
339
|
+
app_resources_dir: utf8_path(app_resources_dir)?,
|
|
340
|
+
dry_run: args.dry_run,
|
|
341
|
+
status: PackageStatus::Planned,
|
|
342
|
+
create_dirs: create_dirs
|
|
343
|
+
.into_iter()
|
|
344
|
+
.map(utf8_path)
|
|
345
|
+
.collect::<Result<Vec<_>>>()?,
|
|
346
|
+
copy_steps: copy_steps
|
|
347
|
+
.into_iter()
|
|
348
|
+
.map(|(from, to)| {
|
|
349
|
+
Ok(CopyStep {
|
|
350
|
+
from: utf8_path(from)?,
|
|
351
|
+
to: utf8_path(to)?,
|
|
352
|
+
})
|
|
353
|
+
})
|
|
354
|
+
.collect::<Result<Vec<_>>>()?,
|
|
355
|
+
warnings,
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
pub(crate) fn execute_package(report: &PackageReport, force: bool) -> Result<()> {
|
|
360
|
+
if report.project.package_json.is_none() {
|
|
361
|
+
bail!("No package.json found. Run electron-cli package inside an Electron project.");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if report.project.electron_dependency.is_none() {
|
|
365
|
+
bail!("No electron dependency found. Install Electron before packaging the app.");
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if report.platform != current_platform() {
|
|
369
|
+
bail!(
|
|
370
|
+
"Cross-platform packaging is not implemented yet. Requested {}, host is {}.",
|
|
371
|
+
report.platform,
|
|
372
|
+
current_platform()
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if report.arch != current_arch() {
|
|
377
|
+
bail!(
|
|
378
|
+
"Cross-architecture packaging is not implemented yet. Requested {}, host is {}.",
|
|
379
|
+
report.arch,
|
|
380
|
+
current_arch()
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
let bundle_dir = Path::new(report.bundle_dir.as_str());
|
|
385
|
+
let package_root = package_root(bundle_dir, &report.platform);
|
|
386
|
+
let app_resources_dir = Path::new(report.app_resources_dir.as_str());
|
|
387
|
+
let app_dir = app_resources_dir.join("app");
|
|
388
|
+
|
|
389
|
+
if package_root.exists() {
|
|
390
|
+
if force {
|
|
391
|
+
fs::remove_dir_all(&package_root)
|
|
392
|
+
.with_context(|| format!("Could not remove {}", package_root.display()))?;
|
|
393
|
+
} else {
|
|
394
|
+
bail!(
|
|
395
|
+
"Package output already exists: {}. Use --force to overwrite it.",
|
|
396
|
+
package_root.display()
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
let electron_source = Path::new(report.copy_steps[0].from.as_str());
|
|
402
|
+
if !electron_source.exists() {
|
|
403
|
+
bail!(
|
|
404
|
+
"Electron runtime was not found at {}. Run your package manager install first.",
|
|
405
|
+
electron_source.display()
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
fs::create_dir_all(&package_root)
|
|
410
|
+
.with_context(|| format!("Could not create {}", package_root.display()))?;
|
|
411
|
+
copy_recursively(electron_source, bundle_dir).with_context(|| {
|
|
412
|
+
format!(
|
|
413
|
+
"Could not copy Electron runtime to {}",
|
|
414
|
+
bundle_dir.display()
|
|
415
|
+
)
|
|
416
|
+
})?;
|
|
417
|
+
rename_runtime_executable(bundle_dir, &report.executable_name, &report.platform)?;
|
|
418
|
+
apply_package_metadata(report)?;
|
|
419
|
+
copy_package_resources(report)?;
|
|
420
|
+
|
|
421
|
+
fs::create_dir_all(&app_dir)
|
|
422
|
+
.with_context(|| format!("Could not create {}", app_dir.display()))?;
|
|
423
|
+
copy_project_files(
|
|
424
|
+
Path::new(report.project.root.as_str()),
|
|
425
|
+
&app_dir,
|
|
426
|
+
Path::new(report.output_dir.as_str()),
|
|
427
|
+
)?;
|
|
428
|
+
copy_runtime_dependencies(
|
|
429
|
+
Path::new(report.project.root.as_str()),
|
|
430
|
+
&app_dir,
|
|
431
|
+
&report.project,
|
|
432
|
+
)?;
|
|
433
|
+
execute_macos_signing(report)?;
|
|
434
|
+
execute_macos_notarization(report)?;
|
|
435
|
+
|
|
436
|
+
Ok(())
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
fn execute_macos_signing(report: &PackageReport) -> Result<()> {
|
|
440
|
+
if report.platform != "darwin" || !report.signing.macos.sign.will_execute {
|
|
441
|
+
return Ok(());
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
let bundle_dir = Path::new(report.bundle_dir.as_str());
|
|
445
|
+
let bundle_parent = bundle_dir
|
|
446
|
+
.parent()
|
|
447
|
+
.context("macOS bundle output has no parent directory")?;
|
|
448
|
+
let bundle_name = bundle_dir
|
|
449
|
+
.file_name()
|
|
450
|
+
.context("macOS bundle output has no bundle directory name")?;
|
|
451
|
+
let unique_suffix = std::time::SystemTime::now()
|
|
452
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
453
|
+
.context("system clock is before the Unix epoch")?
|
|
454
|
+
.as_nanos();
|
|
455
|
+
let signing_parent = bundle_parent.join(format!(
|
|
456
|
+
".electron-cli-signing-{}-{unique_suffix}",
|
|
457
|
+
std::process::id()
|
|
458
|
+
));
|
|
459
|
+
let signed_bundle_dir = signing_parent.join(bundle_name);
|
|
460
|
+
|
|
461
|
+
if signing_parent.exists() {
|
|
462
|
+
fs::remove_dir_all(&signing_parent)
|
|
463
|
+
.with_context(|| format!("Could not remove {}", signing_parent.display()))?;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
let signing_result = (|| -> Result<()> {
|
|
467
|
+
let mut signer = BundleSigner::new_from_path(bundle_dir).with_context(|| {
|
|
468
|
+
format!(
|
|
469
|
+
"Could not prepare macOS bundle signing for {}",
|
|
470
|
+
bundle_dir.display()
|
|
471
|
+
)
|
|
472
|
+
})?;
|
|
473
|
+
signer
|
|
474
|
+
.collect_nested_bundles()
|
|
475
|
+
.context("Could not discover nested macOS bundles for signing")?;
|
|
476
|
+
|
|
477
|
+
let mut settings = macos_signing_settings(report)?;
|
|
478
|
+
if let Some(p12_file) = &report.signing.macos.sign.p12_file {
|
|
479
|
+
let p12_path = Path::new(p12_file.as_str());
|
|
480
|
+
let p12_data = fs::read(p12_path)
|
|
481
|
+
.with_context(|| format!("Could not read {}", p12_path.display()))?;
|
|
482
|
+
let password = macos_p12_password(&report.signing.macos.sign)?;
|
|
483
|
+
let (certificate, signing_key) = parse_pfx_data(&p12_data, &password)
|
|
484
|
+
.with_context(|| format!("Could not parse {}", p12_path.display()))?;
|
|
485
|
+
|
|
486
|
+
settings.set_signing_key(signing_key.as_key_info_signer(), certificate);
|
|
487
|
+
settings.chain_apple_certificates();
|
|
488
|
+
settings.set_team_id_from_signing_certificate();
|
|
489
|
+
settings
|
|
490
|
+
.ensure_for_notarization_settings()
|
|
491
|
+
.context("macOS signing settings are not compatible with notarization")?;
|
|
492
|
+
signer
|
|
493
|
+
.write_signed_bundle(&signed_bundle_dir, &settings)
|
|
494
|
+
.with_context(|| {
|
|
495
|
+
format!(
|
|
496
|
+
"Could not write signed macOS bundle to {}",
|
|
497
|
+
signed_bundle_dir.display()
|
|
498
|
+
)
|
|
499
|
+
})?;
|
|
500
|
+
} else {
|
|
501
|
+
signer
|
|
502
|
+
.write_signed_bundle(&signed_bundle_dir, &settings)
|
|
503
|
+
.with_context(|| {
|
|
504
|
+
format!(
|
|
505
|
+
"Could not write signed macOS bundle to {}",
|
|
506
|
+
signed_bundle_dir.display()
|
|
507
|
+
)
|
|
508
|
+
})?;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
Ok(())
|
|
512
|
+
})();
|
|
513
|
+
|
|
514
|
+
if let Err(error) = signing_result {
|
|
515
|
+
let _ = fs::remove_dir_all(&signing_parent);
|
|
516
|
+
return Err(error);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
fs::remove_dir_all(bundle_dir)
|
|
520
|
+
.with_context(|| format!("Could not remove {}", bundle_dir.display()))?;
|
|
521
|
+
fs::rename(&signed_bundle_dir, bundle_dir).with_context(|| {
|
|
522
|
+
format!(
|
|
523
|
+
"Could not move signed macOS bundle from {} to {}",
|
|
524
|
+
signed_bundle_dir.display(),
|
|
525
|
+
bundle_dir.display()
|
|
526
|
+
)
|
|
527
|
+
})?;
|
|
528
|
+
let _ = fs::remove_dir_all(&signing_parent);
|
|
529
|
+
|
|
530
|
+
Ok(())
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
fn execute_macos_notarization(report: &PackageReport) -> Result<()> {
|
|
534
|
+
if report.platform != "darwin" || !report.signing.macos.notarize.will_execute {
|
|
535
|
+
return Ok(());
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
let notarize = &report.signing.macos.notarize;
|
|
539
|
+
let bundle_dir = Path::new(report.bundle_dir.as_str());
|
|
540
|
+
let wait_limit = notarize.wait.then_some(std::time::Duration::from_secs(
|
|
541
|
+
notarize.wait_timeout_seconds,
|
|
542
|
+
));
|
|
543
|
+
let notarizer = macos_notarizer(notarize)?;
|
|
544
|
+
let upload = notarizer
|
|
545
|
+
.notarize_path(bundle_dir, wait_limit)
|
|
546
|
+
.with_context(|| format!("Could not notarize macOS bundle {}", bundle_dir.display()))?;
|
|
547
|
+
|
|
548
|
+
if notarize.staple {
|
|
549
|
+
match upload {
|
|
550
|
+
NotarizationUpload::NotaryResponse(_) => {
|
|
551
|
+
let stapler =
|
|
552
|
+
Stapler::new().context("Could not prepare macOS notarization stapler")?;
|
|
553
|
+
stapler.staple_path(bundle_dir).with_context(|| {
|
|
554
|
+
format!(
|
|
555
|
+
"Could not staple notarization ticket to {}",
|
|
556
|
+
bundle_dir.display()
|
|
557
|
+
)
|
|
558
|
+
})?;
|
|
559
|
+
}
|
|
560
|
+
NotarizationUpload::UploadId(upload_id) => {
|
|
561
|
+
bail!(
|
|
562
|
+
"macOS notarization upload {upload_id} was submitted without waiting; stapling requires a completed notarization result."
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
Ok(())
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
fn macos_notarizer(notarize: &MacosNotarizePlan) -> Result<Notarizer> {
|
|
572
|
+
let api_key_path = notarize
|
|
573
|
+
.apple_api_key
|
|
574
|
+
.as_ref()
|
|
575
|
+
.context("macOS notarization requires appleApiKey")?;
|
|
576
|
+
let api_key_path = Path::new(api_key_path.as_str());
|
|
577
|
+
let key_id = notarize
|
|
578
|
+
.apple_api_key_id
|
|
579
|
+
.as_deref()
|
|
580
|
+
.context("macOS notarization requires appleApiKeyId")?;
|
|
581
|
+
let issuer = notarize
|
|
582
|
+
.apple_api_issuer
|
|
583
|
+
.as_deref()
|
|
584
|
+
.context("macOS notarization requires appleApiIssuer")?;
|
|
585
|
+
|
|
586
|
+
if path_extension(api_key_path) == Some("json") {
|
|
587
|
+
return Notarizer::from_api_key(api_key_path)
|
|
588
|
+
.with_context(|| format!("Could not load Apple API key {}", api_key_path.display()));
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
let temp_api_key = temporary_unified_api_key(issuer, key_id, api_key_path)?;
|
|
592
|
+
Notarizer::from_api_key(&temp_api_key.path)
|
|
593
|
+
.with_context(|| format!("Could not load Apple API key {}", api_key_path.display()))
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
struct TemporaryFile {
|
|
597
|
+
path: PathBuf,
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
impl Drop for TemporaryFile {
|
|
601
|
+
fn drop(&mut self) {
|
|
602
|
+
let _ = fs::remove_file(&self.path);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
fn temporary_unified_api_key(
|
|
607
|
+
issuer: &str,
|
|
608
|
+
key_id: &str,
|
|
609
|
+
private_key_path: &Path,
|
|
610
|
+
) -> Result<TemporaryFile> {
|
|
611
|
+
let unique_suffix = std::time::SystemTime::now()
|
|
612
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
613
|
+
.context("system clock is before the Unix epoch")?
|
|
614
|
+
.as_nanos();
|
|
615
|
+
let path = std::env::temp_dir().join(format!(
|
|
616
|
+
"electron-cli-notary-key-{}-{unique_suffix}.json",
|
|
617
|
+
std::process::id()
|
|
618
|
+
));
|
|
619
|
+
let unified = UnifiedApiKey::from_ecdsa_pem_path(issuer, key_id, private_key_path)
|
|
620
|
+
.with_context(|| {
|
|
621
|
+
format!(
|
|
622
|
+
"Could not read Apple API private key {}",
|
|
623
|
+
private_key_path.display()
|
|
624
|
+
)
|
|
625
|
+
})?;
|
|
626
|
+
unified
|
|
627
|
+
.write_json_file(&path)
|
|
628
|
+
.with_context(|| format!("Could not write temporary Apple API key {}", path.display()))?;
|
|
629
|
+
|
|
630
|
+
Ok(TemporaryFile { path })
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
fn macos_signing_settings<'key>(report: &PackageReport) -> Result<SigningSettings<'key>> {
|
|
634
|
+
let sign = &report.signing.macos.sign;
|
|
635
|
+
let mut settings = SigningSettings::default();
|
|
636
|
+
settings.set_binary_identifier(SettingsScope::Main, &report.metadata.bundle_identifier);
|
|
637
|
+
settings.set_for_notarization(sign.for_notarization);
|
|
638
|
+
|
|
639
|
+
if let Some(timestamp_url) = &sign.timestamp_url {
|
|
640
|
+
settings
|
|
641
|
+
.set_time_stamp_url(timestamp_url)
|
|
642
|
+
.with_context(|| format!("Invalid macOS signing timestamp URL: {timestamp_url}"))?;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if sign.hardened_runtime.unwrap_or(false) {
|
|
646
|
+
settings.add_code_signature_flags(SettingsScope::Main, CodeSignatureFlags::RUNTIME);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if let Some(entitlements) = sign.entitlements.first() {
|
|
650
|
+
let entitlements_path = Path::new(entitlements.as_str());
|
|
651
|
+
let entitlements_xml = fs::read_to_string(entitlements_path).with_context(|| {
|
|
652
|
+
format!(
|
|
653
|
+
"Could not read macOS entitlements file {}",
|
|
654
|
+
entitlements_path.display()
|
|
655
|
+
)
|
|
656
|
+
})?;
|
|
657
|
+
settings
|
|
658
|
+
.set_entitlements_xml(SettingsScope::Main, entitlements_xml)
|
|
659
|
+
.with_context(|| {
|
|
660
|
+
format!(
|
|
661
|
+
"Could not parse macOS entitlements file {}",
|
|
662
|
+
entitlements_path.display()
|
|
663
|
+
)
|
|
664
|
+
})?;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
Ok(settings)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
fn macos_p12_password(sign: &MacosSignPlan) -> Result<String> {
|
|
671
|
+
if let Some(password) = sign.p12_password.as_deref() {
|
|
672
|
+
return Ok(password.to_string());
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if let Some(env_name) = &sign.p12_password_env {
|
|
676
|
+
return std::env::var(env_name)
|
|
677
|
+
.with_context(|| format!("Could not read macOS signing p12 password env {env_name}"));
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if let Some(path) = &sign.p12_password_file {
|
|
681
|
+
let password_path = Path::new(path.as_str());
|
|
682
|
+
return fs::read_to_string(password_path)
|
|
683
|
+
.with_context(|| {
|
|
684
|
+
format!(
|
|
685
|
+
"Could not read macOS signing p12 password file {}",
|
|
686
|
+
password_path.display()
|
|
687
|
+
)
|
|
688
|
+
})
|
|
689
|
+
.and_then(|contents| {
|
|
690
|
+
contents
|
|
691
|
+
.lines()
|
|
692
|
+
.next()
|
|
693
|
+
.map(str::to_string)
|
|
694
|
+
.context("macOS signing p12 password file is empty")
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
Ok(String::new())
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
fn print_report(report: &PackageReport, json: bool) -> Result<()> {
|
|
702
|
+
if json {
|
|
703
|
+
return output::json(report);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
println!("electron-cli package");
|
|
707
|
+
println!();
|
|
708
|
+
println!("Project");
|
|
709
|
+
println!(" root: {}", report.project.root);
|
|
710
|
+
match report.project.package_label() {
|
|
711
|
+
Some(label) => println!(" package: {label}"),
|
|
712
|
+
None => println!(" package: not found"),
|
|
713
|
+
}
|
|
714
|
+
println!(" app name: {}", report.app_name);
|
|
715
|
+
println!(" executable: {}", report.executable_name);
|
|
716
|
+
println!(" bundle id: {}", report.metadata.bundle_identifier);
|
|
717
|
+
if let Some(version) = &report.metadata.app_version {
|
|
718
|
+
println!(" app version: {version}");
|
|
719
|
+
}
|
|
720
|
+
println!(" target: {} {}", report.platform, report.arch);
|
|
721
|
+
println!(" status: {}", report.status.as_str());
|
|
722
|
+
|
|
723
|
+
if report.signing.macos.sign.configured || report.signing.macos.notarize.configured {
|
|
724
|
+
println!();
|
|
725
|
+
println!("Signing");
|
|
726
|
+
println!(
|
|
727
|
+
" macOS signing: {}",
|
|
728
|
+
if report.signing.macos.sign.enabled {
|
|
729
|
+
"configured"
|
|
730
|
+
} else {
|
|
731
|
+
"disabled"
|
|
732
|
+
}
|
|
733
|
+
);
|
|
734
|
+
if let Some(identity) = &report.signing.macos.sign.identity {
|
|
735
|
+
println!(" identity: {identity}");
|
|
736
|
+
}
|
|
737
|
+
if let Some(path) = &report.signing.macos.sign.p12_file {
|
|
738
|
+
println!(" p12 file: {path}");
|
|
739
|
+
}
|
|
740
|
+
if let Some(source) = &report.signing.macos.sign.p12_password_source {
|
|
741
|
+
println!(" p12 password: {source}");
|
|
742
|
+
}
|
|
743
|
+
if let Some(timestamp_url) = &report.signing.macos.sign.timestamp_url {
|
|
744
|
+
println!(" timestamp server: {timestamp_url}");
|
|
745
|
+
}
|
|
746
|
+
if report.signing.macos.sign.for_notarization {
|
|
747
|
+
println!(" signing mode: notarization-compatible");
|
|
748
|
+
}
|
|
749
|
+
if let Some(method) = &report.signing.macos.sign.method {
|
|
750
|
+
println!(" signing method: {method}");
|
|
751
|
+
}
|
|
752
|
+
println!(
|
|
753
|
+
" signing execution: {}",
|
|
754
|
+
if report.signing.macos.sign.will_execute {
|
|
755
|
+
"enabled"
|
|
756
|
+
} else {
|
|
757
|
+
"not available"
|
|
758
|
+
}
|
|
759
|
+
);
|
|
760
|
+
println!(
|
|
761
|
+
" macOS notarization: {}",
|
|
762
|
+
if report.signing.macos.notarize.enabled {
|
|
763
|
+
"configured"
|
|
764
|
+
} else {
|
|
765
|
+
"disabled"
|
|
766
|
+
}
|
|
767
|
+
);
|
|
768
|
+
if let Some(method) = &report.signing.macos.notarize.auth_method {
|
|
769
|
+
println!(" notarization auth: {method}");
|
|
770
|
+
}
|
|
771
|
+
if let Some(path) = &report.signing.macos.notarize.apple_api_key {
|
|
772
|
+
println!(" Apple API key: {path}");
|
|
773
|
+
}
|
|
774
|
+
println!(
|
|
775
|
+
" notarization execution: {}",
|
|
776
|
+
if report.signing.macos.notarize.will_execute {
|
|
777
|
+
"enabled"
|
|
778
|
+
} else {
|
|
779
|
+
"not available"
|
|
780
|
+
}
|
|
781
|
+
);
|
|
782
|
+
if report.signing.macos.notarize.will_execute {
|
|
783
|
+
println!(
|
|
784
|
+
" notarization wait: {}",
|
|
785
|
+
if report.signing.macos.notarize.wait {
|
|
786
|
+
format!("{}s", report.signing.macos.notarize.wait_timeout_seconds)
|
|
787
|
+
} else {
|
|
788
|
+
"disabled".to_string()
|
|
789
|
+
}
|
|
790
|
+
);
|
|
791
|
+
println!(
|
|
792
|
+
" notarization stapling: {}",
|
|
793
|
+
if report.signing.macos.notarize.staple {
|
|
794
|
+
"enabled"
|
|
795
|
+
} else {
|
|
796
|
+
"disabled"
|
|
797
|
+
}
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
println!();
|
|
803
|
+
println!("Output");
|
|
804
|
+
println!(" {}", report.bundle_dir);
|
|
805
|
+
|
|
806
|
+
println!();
|
|
807
|
+
println!("Copy");
|
|
808
|
+
for step in &report.copy_steps {
|
|
809
|
+
println!(" {} -> {}", step.from, step.to);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if !report.warnings.is_empty() {
|
|
813
|
+
println!();
|
|
814
|
+
println!("Warnings");
|
|
815
|
+
for warning in &report.warnings {
|
|
816
|
+
println!(" {warning}");
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
Ok(())
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
fn read_package_json_config(snapshot: &ProjectSnapshot) -> Result<PackageJsonConfig> {
|
|
824
|
+
let project_config = crate::forge_config::read(snapshot)?;
|
|
825
|
+
|
|
826
|
+
let mut packager = PackagerConfig::default();
|
|
827
|
+
if let Some(config) = project_config
|
|
828
|
+
.forge()
|
|
829
|
+
.and_then(|forge| forge.get("packagerConfig"))
|
|
830
|
+
{
|
|
831
|
+
packager.merge(parse_packager_config(config));
|
|
832
|
+
}
|
|
833
|
+
if let Some(config) = project_config
|
|
834
|
+
.package()
|
|
835
|
+
.and_then(|package| package.get("electronPackagerConfig"))
|
|
836
|
+
{
|
|
837
|
+
packager.merge(parse_packager_config(config));
|
|
838
|
+
}
|
|
839
|
+
if let Some(config) = project_config
|
|
840
|
+
.electron_cli()
|
|
841
|
+
.and_then(|config| config.get("packagerConfig"))
|
|
842
|
+
{
|
|
843
|
+
packager.merge(parse_packager_config(config));
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
Ok(PackageJsonConfig {
|
|
847
|
+
product_name: project_config
|
|
848
|
+
.package()
|
|
849
|
+
.and_then(|package| package.get("productName"))
|
|
850
|
+
.and_then(JsonValue::as_str)
|
|
851
|
+
.map(ToOwned::to_owned),
|
|
852
|
+
app_version: project_config
|
|
853
|
+
.package()
|
|
854
|
+
.and_then(|package| package.get("version"))
|
|
855
|
+
.and_then(JsonValue::as_str)
|
|
856
|
+
.map(ToOwned::to_owned),
|
|
857
|
+
warnings: project_config.warnings().to_vec(),
|
|
858
|
+
packager,
|
|
859
|
+
})
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
fn parse_packager_config(value: &JsonValue) -> PackagerConfig {
|
|
863
|
+
PackagerConfig {
|
|
864
|
+
name: string_value(value, "name"),
|
|
865
|
+
executable_name: string_value(value, "executableName"),
|
|
866
|
+
app_bundle_id: string_value(value, "appBundleId"),
|
|
867
|
+
app_category_type: string_value(value, "appCategoryType"),
|
|
868
|
+
app_version: string_value(value, "appVersion"),
|
|
869
|
+
build_version: string_value(value, "buildVersion"),
|
|
870
|
+
app_copyright: string_value(value, "appCopyright"),
|
|
871
|
+
icon: string_list(value.get("icon")),
|
|
872
|
+
extra_resource: string_list(value.get("extraResource")),
|
|
873
|
+
darwin_dark_mode_support: value
|
|
874
|
+
.get("darwinDarkModeSupport")
|
|
875
|
+
.and_then(JsonValue::as_bool)
|
|
876
|
+
.unwrap_or(false),
|
|
877
|
+
osx_sign: parse_macos_sign_config(value.get("osxSign")),
|
|
878
|
+
osx_notarize: parse_macos_notarize_config(value.get("osxNotarize")),
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
fn parse_macos_sign_config(value: Option<&JsonValue>) -> MacosSignConfig {
|
|
883
|
+
match value {
|
|
884
|
+
None => MacosSignConfig::default(),
|
|
885
|
+
Some(JsonValue::Bool(false)) => MacosSignConfig {
|
|
886
|
+
configured: true,
|
|
887
|
+
enabled: false,
|
|
888
|
+
..MacosSignConfig::default()
|
|
889
|
+
},
|
|
890
|
+
Some(JsonValue::Bool(true)) => MacosSignConfig {
|
|
891
|
+
configured: true,
|
|
892
|
+
enabled: true,
|
|
893
|
+
..MacosSignConfig::default()
|
|
894
|
+
},
|
|
895
|
+
Some(JsonValue::Object(object)) => {
|
|
896
|
+
let entitlements = [
|
|
897
|
+
"entitlements",
|
|
898
|
+
"entitlementsInherit",
|
|
899
|
+
"entitlementsLoginHelper",
|
|
900
|
+
]
|
|
901
|
+
.iter()
|
|
902
|
+
.filter_map(|key| {
|
|
903
|
+
object
|
|
904
|
+
.get(*key)
|
|
905
|
+
.and_then(JsonValue::as_str)
|
|
906
|
+
.map(ToOwned::to_owned)
|
|
907
|
+
})
|
|
908
|
+
.collect();
|
|
909
|
+
|
|
910
|
+
MacosSignConfig {
|
|
911
|
+
configured: true,
|
|
912
|
+
enabled: true,
|
|
913
|
+
invalid_type: false,
|
|
914
|
+
identity: object
|
|
915
|
+
.get("identity")
|
|
916
|
+
.or_else(|| object.get("identityName"))
|
|
917
|
+
.and_then(JsonValue::as_str)
|
|
918
|
+
.map(ToOwned::to_owned),
|
|
919
|
+
p12_file: object
|
|
920
|
+
.get("p12File")
|
|
921
|
+
.or_else(|| object.get("pfxFile"))
|
|
922
|
+
.and_then(JsonValue::as_str)
|
|
923
|
+
.map(ToOwned::to_owned),
|
|
924
|
+
p12_password: object
|
|
925
|
+
.get("p12Password")
|
|
926
|
+
.or_else(|| object.get("pfxPassword"))
|
|
927
|
+
.and_then(JsonValue::as_str)
|
|
928
|
+
.map(ToOwned::to_owned),
|
|
929
|
+
p12_password_env: object
|
|
930
|
+
.get("p12PasswordEnv")
|
|
931
|
+
.or_else(|| object.get("pfxPasswordEnv"))
|
|
932
|
+
.and_then(JsonValue::as_str)
|
|
933
|
+
.map(ToOwned::to_owned),
|
|
934
|
+
p12_password_file: object
|
|
935
|
+
.get("p12PasswordFile")
|
|
936
|
+
.or_else(|| object.get("pfxPasswordFile"))
|
|
937
|
+
.and_then(JsonValue::as_str)
|
|
938
|
+
.map(ToOwned::to_owned),
|
|
939
|
+
timestamp: parse_macos_timestamp_config(
|
|
940
|
+
object
|
|
941
|
+
.get("timestamp")
|
|
942
|
+
.or_else(|| object.get("timestampUrl"))
|
|
943
|
+
.or_else(|| object.get("timestampURL")),
|
|
944
|
+
),
|
|
945
|
+
entitlements,
|
|
946
|
+
entitlements_inherit: object
|
|
947
|
+
.get("entitlementsInherit")
|
|
948
|
+
.and_then(JsonValue::as_str)
|
|
949
|
+
.map(ToOwned::to_owned),
|
|
950
|
+
hardened_runtime: object.get("hardenedRuntime").and_then(JsonValue::as_bool),
|
|
951
|
+
gatekeeper_assess: object.get("gatekeeperAssess").and_then(JsonValue::as_bool),
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
Some(_) => MacosSignConfig {
|
|
955
|
+
configured: true,
|
|
956
|
+
invalid_type: true,
|
|
957
|
+
..MacosSignConfig::default()
|
|
958
|
+
},
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
fn parse_macos_timestamp_config(value: Option<&JsonValue>) -> Option<MacosTimestampConfig> {
|
|
963
|
+
match value {
|
|
964
|
+
Some(JsonValue::String(value)) => {
|
|
965
|
+
let value = value.trim();
|
|
966
|
+
if value.is_empty() || value.eq_ignore_ascii_case("none") {
|
|
967
|
+
Some(MacosTimestampConfig::Disabled)
|
|
968
|
+
} else {
|
|
969
|
+
Some(MacosTimestampConfig::Url(value.to_string()))
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
Some(JsonValue::Bool(true)) => Some(MacosTimestampConfig::Default),
|
|
973
|
+
Some(JsonValue::Bool(false)) => Some(MacosTimestampConfig::Disabled),
|
|
974
|
+
_ => None,
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
fn parse_macos_notarize_config(value: Option<&JsonValue>) -> MacosNotarizeConfig {
|
|
979
|
+
match value {
|
|
980
|
+
None => MacosNotarizeConfig::default(),
|
|
981
|
+
Some(JsonValue::Bool(false)) => MacosNotarizeConfig {
|
|
982
|
+
configured: true,
|
|
983
|
+
enabled: false,
|
|
984
|
+
..MacosNotarizeConfig::default()
|
|
985
|
+
},
|
|
986
|
+
Some(JsonValue::Bool(true)) => MacosNotarizeConfig {
|
|
987
|
+
configured: true,
|
|
988
|
+
enabled: true,
|
|
989
|
+
..MacosNotarizeConfig::default()
|
|
990
|
+
},
|
|
991
|
+
Some(JsonValue::Object(object)) => MacosNotarizeConfig {
|
|
992
|
+
configured: true,
|
|
993
|
+
enabled: true,
|
|
994
|
+
invalid_type: false,
|
|
995
|
+
apple_id_set: object.get("appleId").and_then(JsonValue::as_str).is_some(),
|
|
996
|
+
apple_id_password_set: object
|
|
997
|
+
.get("appleIdPassword")
|
|
998
|
+
.and_then(JsonValue::as_str)
|
|
999
|
+
.is_some(),
|
|
1000
|
+
team_id_set: object.get("teamId").and_then(JsonValue::as_str).is_some(),
|
|
1001
|
+
apple_api_key: object
|
|
1002
|
+
.get("appleApiKey")
|
|
1003
|
+
.and_then(JsonValue::as_str)
|
|
1004
|
+
.map(ToOwned::to_owned),
|
|
1005
|
+
apple_api_key_id: object
|
|
1006
|
+
.get("appleApiKeyId")
|
|
1007
|
+
.and_then(JsonValue::as_str)
|
|
1008
|
+
.map(ToOwned::to_owned),
|
|
1009
|
+
apple_api_issuer: object
|
|
1010
|
+
.get("appleApiIssuer")
|
|
1011
|
+
.and_then(JsonValue::as_str)
|
|
1012
|
+
.map(ToOwned::to_owned),
|
|
1013
|
+
keychain_profile: object
|
|
1014
|
+
.get("keychainProfile")
|
|
1015
|
+
.and_then(JsonValue::as_str)
|
|
1016
|
+
.map(ToOwned::to_owned),
|
|
1017
|
+
keychain: object
|
|
1018
|
+
.get("keychain")
|
|
1019
|
+
.and_then(JsonValue::as_str)
|
|
1020
|
+
.map(ToOwned::to_owned),
|
|
1021
|
+
wait: object.get("wait").and_then(JsonValue::as_bool),
|
|
1022
|
+
wait_timeout_seconds: object
|
|
1023
|
+
.get("maxWaitSeconds")
|
|
1024
|
+
.or_else(|| object.get("waitTimeoutSeconds"))
|
|
1025
|
+
.and_then(JsonValue::as_u64),
|
|
1026
|
+
staple: object.get("staple").and_then(JsonValue::as_bool),
|
|
1027
|
+
},
|
|
1028
|
+
Some(_) => MacosNotarizeConfig {
|
|
1029
|
+
configured: true,
|
|
1030
|
+
invalid_type: true,
|
|
1031
|
+
..MacosNotarizeConfig::default()
|
|
1032
|
+
},
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
fn string_value(value: &JsonValue, key: &str) -> Option<String> {
|
|
1037
|
+
value
|
|
1038
|
+
.get(key)
|
|
1039
|
+
.and_then(JsonValue::as_str)
|
|
1040
|
+
.map(ToOwned::to_owned)
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
fn string_list(value: Option<&JsonValue>) -> Vec<String> {
|
|
1044
|
+
match value {
|
|
1045
|
+
Some(JsonValue::String(value)) => vec![value.clone()],
|
|
1046
|
+
Some(JsonValue::Array(values)) => values
|
|
1047
|
+
.iter()
|
|
1048
|
+
.filter_map(JsonValue::as_str)
|
|
1049
|
+
.map(ToOwned::to_owned)
|
|
1050
|
+
.collect(),
|
|
1051
|
+
_ => Vec::new(),
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
fn package_metadata(
|
|
1056
|
+
root: &Path,
|
|
1057
|
+
config: &PackageJsonConfig,
|
|
1058
|
+
artifact_name: &str,
|
|
1059
|
+
app_resources_dir: &Path,
|
|
1060
|
+
platform: &str,
|
|
1061
|
+
) -> Result<(PackageMetadata, Vec<String>)> {
|
|
1062
|
+
let mut warnings = Vec::new();
|
|
1063
|
+
let icon = resolve_icon_resource(
|
|
1064
|
+
root,
|
|
1065
|
+
&config.packager.icon,
|
|
1066
|
+
artifact_name,
|
|
1067
|
+
app_resources_dir,
|
|
1068
|
+
platform,
|
|
1069
|
+
&mut warnings,
|
|
1070
|
+
)?;
|
|
1071
|
+
let extra_resources = resolve_extra_resources(
|
|
1072
|
+
root,
|
|
1073
|
+
&config.packager.extra_resource,
|
|
1074
|
+
app_resources_dir,
|
|
1075
|
+
&mut warnings,
|
|
1076
|
+
)?;
|
|
1077
|
+
let app_version = config
|
|
1078
|
+
.packager
|
|
1079
|
+
.app_version
|
|
1080
|
+
.clone()
|
|
1081
|
+
.or_else(|| config.app_version.clone());
|
|
1082
|
+
|
|
1083
|
+
Ok((
|
|
1084
|
+
PackageMetadata {
|
|
1085
|
+
bundle_identifier: config
|
|
1086
|
+
.packager
|
|
1087
|
+
.app_bundle_id
|
|
1088
|
+
.clone()
|
|
1089
|
+
.unwrap_or_else(|| default_bundle_identifier(artifact_name)),
|
|
1090
|
+
app_version: app_version.clone(),
|
|
1091
|
+
build_version: config
|
|
1092
|
+
.packager
|
|
1093
|
+
.build_version
|
|
1094
|
+
.clone()
|
|
1095
|
+
.or_else(|| app_version.clone()),
|
|
1096
|
+
app_category_type: config.packager.app_category_type.clone(),
|
|
1097
|
+
app_copyright: config.packager.app_copyright.clone(),
|
|
1098
|
+
icon,
|
|
1099
|
+
extra_resources,
|
|
1100
|
+
darwin_dark_mode_support: config.packager.darwin_dark_mode_support,
|
|
1101
|
+
},
|
|
1102
|
+
warnings,
|
|
1103
|
+
))
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
fn package_signing(
|
|
1107
|
+
root: &Path,
|
|
1108
|
+
config: &PackageJsonConfig,
|
|
1109
|
+
platform: &str,
|
|
1110
|
+
) -> Result<(PackageSigningPlan, Vec<String>)> {
|
|
1111
|
+
let mut warnings = Vec::new();
|
|
1112
|
+
let sign = macos_sign_plan(
|
|
1113
|
+
root,
|
|
1114
|
+
&config.packager.osx_sign,
|
|
1115
|
+
&config.packager.osx_notarize,
|
|
1116
|
+
platform,
|
|
1117
|
+
&mut warnings,
|
|
1118
|
+
)?;
|
|
1119
|
+
let notarize = macos_notarize_plan(root, config, platform, &sign, &mut warnings)?;
|
|
1120
|
+
|
|
1121
|
+
Ok((
|
|
1122
|
+
PackageSigningPlan {
|
|
1123
|
+
macos: MacosSigningPlan { sign, notarize },
|
|
1124
|
+
},
|
|
1125
|
+
warnings,
|
|
1126
|
+
))
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
fn macos_sign_plan(
|
|
1130
|
+
root: &Path,
|
|
1131
|
+
config: &MacosSignConfig,
|
|
1132
|
+
notarize_config: &MacosNotarizeConfig,
|
|
1133
|
+
platform: &str,
|
|
1134
|
+
warnings: &mut Vec<String>,
|
|
1135
|
+
) -> Result<MacosSignPlan> {
|
|
1136
|
+
if config.invalid_type {
|
|
1137
|
+
warnings.push("packagerConfig.osxSign must be false, true, or an object.".to_string());
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
let entitlements = config
|
|
1141
|
+
.entitlements
|
|
1142
|
+
.iter()
|
|
1143
|
+
.filter(|path| !path.trim().is_empty())
|
|
1144
|
+
.map(|path| {
|
|
1145
|
+
let resolved = resolve_project_path(root, path);
|
|
1146
|
+
if !resolved.exists() {
|
|
1147
|
+
warnings.push(format!(
|
|
1148
|
+
"Configured macOS entitlements file does not exist: {}.",
|
|
1149
|
+
resolved.display()
|
|
1150
|
+
));
|
|
1151
|
+
}
|
|
1152
|
+
utf8_path(resolved)
|
|
1153
|
+
})
|
|
1154
|
+
.collect::<Result<Vec<_>>>()?;
|
|
1155
|
+
let entitlements_inherit = config
|
|
1156
|
+
.entitlements_inherit
|
|
1157
|
+
.as_deref()
|
|
1158
|
+
.filter(|path| !path.trim().is_empty())
|
|
1159
|
+
.map(|path| utf8_path(resolve_project_path(root, path)))
|
|
1160
|
+
.transpose()?;
|
|
1161
|
+
if let Some(path) = &entitlements_inherit {
|
|
1162
|
+
if !Path::new(path.as_str()).exists() {
|
|
1163
|
+
warnings.push(format!(
|
|
1164
|
+
"Configured macOS inherited entitlements file does not exist: {}.",
|
|
1165
|
+
path
|
|
1166
|
+
));
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
let p12_file = config
|
|
1171
|
+
.p12_file
|
|
1172
|
+
.as_deref()
|
|
1173
|
+
.filter(|path| !path.trim().is_empty())
|
|
1174
|
+
.map(|path| utf8_path(resolve_project_path(root, path)))
|
|
1175
|
+
.transpose()?;
|
|
1176
|
+
if let Some(path) = &p12_file {
|
|
1177
|
+
if !Path::new(path.as_str()).exists() {
|
|
1178
|
+
warnings.push(format!(
|
|
1179
|
+
"Configured macOS signing p12 file does not exist: {}.",
|
|
1180
|
+
path
|
|
1181
|
+
));
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
let p12_password_file = config
|
|
1185
|
+
.p12_password_file
|
|
1186
|
+
.as_deref()
|
|
1187
|
+
.filter(|path| !path.trim().is_empty())
|
|
1188
|
+
.map(|path| utf8_path(resolve_project_path(root, path)))
|
|
1189
|
+
.transpose()?;
|
|
1190
|
+
if let Some(path) = &p12_password_file {
|
|
1191
|
+
if !Path::new(path.as_str()).exists() {
|
|
1192
|
+
warnings.push(format!(
|
|
1193
|
+
"Configured macOS signing p12 password file does not exist: {}.",
|
|
1194
|
+
path
|
|
1195
|
+
));
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
let p12_password_source = if p12_file.is_some() {
|
|
1199
|
+
if config.p12_password.is_some() {
|
|
1200
|
+
Some("config".to_string())
|
|
1201
|
+
} else if let Some(env_name) = config
|
|
1202
|
+
.p12_password_env
|
|
1203
|
+
.as_deref()
|
|
1204
|
+
.filter(|name| !name.trim().is_empty())
|
|
1205
|
+
{
|
|
1206
|
+
Some(format!("env:{env_name}"))
|
|
1207
|
+
} else if let Some(path) = &p12_password_file {
|
|
1208
|
+
Some(format!("file:{path}"))
|
|
1209
|
+
} else {
|
|
1210
|
+
Some("empty".to_string())
|
|
1211
|
+
}
|
|
1212
|
+
} else {
|
|
1213
|
+
None
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
let identity = config.identity.as_deref().map(str::trim);
|
|
1217
|
+
let ad_hoc_identity = matches!(identity, None | Some("-"));
|
|
1218
|
+
let p12_identity = p12_file.is_some();
|
|
1219
|
+
let will_execute = config.enabled && platform == "darwin" && (ad_hoc_identity || p12_identity);
|
|
1220
|
+
let timestamp_url = macos_timestamp_url(config, p12_identity, notarize_config.enabled);
|
|
1221
|
+
let for_notarization =
|
|
1222
|
+
will_execute && p12_identity && notarize_config.enabled && timestamp_url.is_some();
|
|
1223
|
+
let method = if config.enabled && platform == "darwin" {
|
|
1224
|
+
if p12_identity {
|
|
1225
|
+
Some("certificate-p12".to_string())
|
|
1226
|
+
} else if ad_hoc_identity {
|
|
1227
|
+
Some("ad-hoc".to_string())
|
|
1228
|
+
} else {
|
|
1229
|
+
Some("certificate-identity".to_string())
|
|
1230
|
+
}
|
|
1231
|
+
} else {
|
|
1232
|
+
None
|
|
1233
|
+
};
|
|
1234
|
+
|
|
1235
|
+
if config.configured && platform != "darwin" {
|
|
1236
|
+
warnings.push(format!(
|
|
1237
|
+
"macOS signing is configured but ignored for target platform {platform}."
|
|
1238
|
+
));
|
|
1239
|
+
} else if config.enabled && !will_execute {
|
|
1240
|
+
warnings.push(
|
|
1241
|
+
"macOS signing identity is configured, but Rust-native keychain identity signing is not implemented yet; package output will be unsigned. Use p12File for certificate signing, or identity '-' / omit identity for experimental ad-hoc signing.".to_string(),
|
|
1242
|
+
);
|
|
1243
|
+
} else if will_execute {
|
|
1244
|
+
if p12_identity && identity.is_some() {
|
|
1245
|
+
warnings.push(
|
|
1246
|
+
"packagerConfig.osxSign.p12File supplies the signing certificate; identity is reported but not used for keychain lookup.".to_string(),
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
if config.entitlements.len() > 1 {
|
|
1250
|
+
warnings.push(
|
|
1251
|
+
"Rust-native macOS signing applies the first macOS entitlements file only; inherited/login-helper entitlement scoping is not implemented yet.".to_string(),
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
if config.entitlements_inherit.is_some() {
|
|
1255
|
+
warnings.push(
|
|
1256
|
+
"packagerConfig.osxSign.entitlementsInherit is recognized but not applied to nested bundles by Rust-native signing yet.".to_string(),
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
if config.gatekeeper_assess.is_some() {
|
|
1260
|
+
warnings.push(
|
|
1261
|
+
"packagerConfig.osxSign.gatekeeperAssess is recognized but Gatekeeper assessment is not implemented yet.".to_string(),
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
if config.timestamp.is_some() && !p12_identity {
|
|
1265
|
+
warnings.push(
|
|
1266
|
+
"packagerConfig.osxSign.timestamp is recognized but ignored without p12File certificate signing.".to_string(),
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
if notarize_config.enabled && p12_identity && timestamp_url.is_none() {
|
|
1270
|
+
warnings.push(
|
|
1271
|
+
"macOS notarization requires a secure timestamp; packagerConfig.osxSign.timestamp disabled timestamping.".to_string(),
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
Ok(MacosSignPlan {
|
|
1277
|
+
configured: config.configured,
|
|
1278
|
+
enabled: config.enabled,
|
|
1279
|
+
will_execute,
|
|
1280
|
+
method,
|
|
1281
|
+
identity: config.identity.clone(),
|
|
1282
|
+
p12_file,
|
|
1283
|
+
p12_password_source,
|
|
1284
|
+
p12_password_env: config.p12_password_env.clone(),
|
|
1285
|
+
p12_password_file,
|
|
1286
|
+
p12_password: RedactedSecret::new(config.p12_password.clone()),
|
|
1287
|
+
timestamp_url,
|
|
1288
|
+
for_notarization,
|
|
1289
|
+
entitlements,
|
|
1290
|
+
entitlements_inherit,
|
|
1291
|
+
hardened_runtime: config.hardened_runtime,
|
|
1292
|
+
gatekeeper_assess: config.gatekeeper_assess,
|
|
1293
|
+
})
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
fn macos_timestamp_url(
|
|
1297
|
+
config: &MacosSignConfig,
|
|
1298
|
+
p12_identity: bool,
|
|
1299
|
+
notarize_enabled: bool,
|
|
1300
|
+
) -> Option<String> {
|
|
1301
|
+
if !p12_identity {
|
|
1302
|
+
return None;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
match &config.timestamp {
|
|
1306
|
+
Some(MacosTimestampConfig::Default) => Some(APPLE_TIMESTAMP_URL.to_string()),
|
|
1307
|
+
Some(MacosTimestampConfig::Disabled) => None,
|
|
1308
|
+
Some(MacosTimestampConfig::Url(url)) => Some(url.clone()),
|
|
1309
|
+
None if notarize_enabled => Some(APPLE_TIMESTAMP_URL.to_string()),
|
|
1310
|
+
None => None,
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
fn macos_notarize_plan(
|
|
1315
|
+
root: &Path,
|
|
1316
|
+
package_config: &PackageJsonConfig,
|
|
1317
|
+
platform: &str,
|
|
1318
|
+
sign: &MacosSignPlan,
|
|
1319
|
+
warnings: &mut Vec<String>,
|
|
1320
|
+
) -> Result<MacosNotarizePlan> {
|
|
1321
|
+
let config = &package_config.packager.osx_notarize;
|
|
1322
|
+
if config.invalid_type {
|
|
1323
|
+
warnings.push("packagerConfig.osxNotarize must be false, true, or an object.".to_string());
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
let auth_method = macos_notarize_auth_method(config);
|
|
1327
|
+
let apple_api_key = config
|
|
1328
|
+
.apple_api_key
|
|
1329
|
+
.as_deref()
|
|
1330
|
+
.filter(|path| !path.trim().is_empty())
|
|
1331
|
+
.map(|path| utf8_path(resolve_project_path(root, path)))
|
|
1332
|
+
.transpose()?;
|
|
1333
|
+
let api_key_auth = auth_method.as_deref() == Some("app-store-connect-api-key");
|
|
1334
|
+
let staple = config.staple.unwrap_or(true);
|
|
1335
|
+
let wait = staple || config.wait.unwrap_or(true);
|
|
1336
|
+
let wait_timeout_seconds = config
|
|
1337
|
+
.wait_timeout_seconds
|
|
1338
|
+
.unwrap_or(MACOS_NOTARIZATION_WAIT_TIMEOUT_SECONDS);
|
|
1339
|
+
let will_execute =
|
|
1340
|
+
config.enabled && platform == "darwin" && sign.for_notarization && api_key_auth;
|
|
1341
|
+
|
|
1342
|
+
if config.configured && platform != "darwin" {
|
|
1343
|
+
warnings.push(format!(
|
|
1344
|
+
"macOS notarization is configured but ignored for target platform {platform}."
|
|
1345
|
+
));
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if config.enabled && !package_config.packager.osx_sign.enabled {
|
|
1349
|
+
warnings.push(
|
|
1350
|
+
"macOS notarization requires packagerConfig.osxSign to be enabled first.".to_string(),
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
if config.enabled
|
|
1354
|
+
&& platform == "darwin"
|
|
1355
|
+
&& package_config.packager.osx_sign.enabled
|
|
1356
|
+
&& package_config.packager.osx_sign.p12_file.is_none()
|
|
1357
|
+
&& matches!(
|
|
1358
|
+
package_config
|
|
1359
|
+
.packager
|
|
1360
|
+
.osx_sign
|
|
1361
|
+
.identity
|
|
1362
|
+
.as_deref()
|
|
1363
|
+
.map(str::trim),
|
|
1364
|
+
None | Some("-")
|
|
1365
|
+
)
|
|
1366
|
+
{
|
|
1367
|
+
warnings.push(
|
|
1368
|
+
"macOS notarization requires a Developer ID signature; Rust-native ad-hoc signing is not notarizable.".to_string(),
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
if config.enabled
|
|
1372
|
+
&& platform == "darwin"
|
|
1373
|
+
&& package_config.packager.osx_sign.enabled
|
|
1374
|
+
&& !sign.for_notarization
|
|
1375
|
+
{
|
|
1376
|
+
warnings.push(
|
|
1377
|
+
"macOS notarization execution requires Rust-native p12File Developer ID signing with a secure timestamp.".to_string(),
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
if config.enabled && auth_method.is_none() {
|
|
1381
|
+
warnings.push(
|
|
1382
|
+
"macOS notarization config is missing a complete notarytool authentication set: appleId/appleIdPassword/teamId, appleApiKey/appleApiKeyId/appleApiIssuer, or keychainProfile.".to_string(),
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
if config.enabled
|
|
1386
|
+
&& platform == "darwin"
|
|
1387
|
+
&& matches!(
|
|
1388
|
+
auth_method.as_deref(),
|
|
1389
|
+
Some("keychain-profile") | Some("apple-id")
|
|
1390
|
+
)
|
|
1391
|
+
{
|
|
1392
|
+
warnings.push(
|
|
1393
|
+
"Rust-native macOS notarization execution currently requires appleApiKey, appleApiKeyId, and appleApiIssuer; keychain profile and Apple ID auth are recognized for planning only.".to_string(),
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
if let Some(path) = &apple_api_key {
|
|
1397
|
+
if !Path::new(path.as_str()).exists() {
|
|
1398
|
+
warnings.push(format!(
|
|
1399
|
+
"Configured Apple API key file does not exist: {}.",
|
|
1400
|
+
path
|
|
1401
|
+
));
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
Ok(MacosNotarizePlan {
|
|
1405
|
+
configured: config.configured,
|
|
1406
|
+
enabled: config.enabled,
|
|
1407
|
+
will_execute,
|
|
1408
|
+
auth_method,
|
|
1409
|
+
apple_api_key,
|
|
1410
|
+
apple_api_key_id: RedactedSecret::new(config.apple_api_key_id.clone()),
|
|
1411
|
+
apple_api_issuer: RedactedSecret::new(config.apple_api_issuer.clone()),
|
|
1412
|
+
keychain_profile: config.keychain_profile.clone(),
|
|
1413
|
+
keychain: config.keychain.clone(),
|
|
1414
|
+
wait,
|
|
1415
|
+
wait_timeout_seconds,
|
|
1416
|
+
staple,
|
|
1417
|
+
})
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
fn macos_notarize_auth_method(config: &MacosNotarizeConfig) -> Option<String> {
|
|
1421
|
+
if config
|
|
1422
|
+
.keychain_profile
|
|
1423
|
+
.as_deref()
|
|
1424
|
+
.is_some_and(|value| !value.trim().is_empty())
|
|
1425
|
+
{
|
|
1426
|
+
Some("keychain-profile".to_string())
|
|
1427
|
+
} else if config.apple_api_key.is_some()
|
|
1428
|
+
&& config
|
|
1429
|
+
.apple_api_key_id
|
|
1430
|
+
.as_deref()
|
|
1431
|
+
.is_some_and(|value| !value.trim().is_empty())
|
|
1432
|
+
&& config
|
|
1433
|
+
.apple_api_issuer
|
|
1434
|
+
.as_deref()
|
|
1435
|
+
.is_some_and(|value| !value.trim().is_empty())
|
|
1436
|
+
{
|
|
1437
|
+
Some("app-store-connect-api-key".to_string())
|
|
1438
|
+
} else if config.apple_id_set && config.apple_id_password_set && config.team_id_set {
|
|
1439
|
+
Some("apple-id".to_string())
|
|
1440
|
+
} else {
|
|
1441
|
+
None
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
fn resolve_icon_resource(
|
|
1446
|
+
root: &Path,
|
|
1447
|
+
configured_icons: &[String],
|
|
1448
|
+
artifact_name: &str,
|
|
1449
|
+
app_resources_dir: &Path,
|
|
1450
|
+
platform: &str,
|
|
1451
|
+
warnings: &mut Vec<String>,
|
|
1452
|
+
) -> Result<Option<IconResource>> {
|
|
1453
|
+
let candidates = configured_icons
|
|
1454
|
+
.iter()
|
|
1455
|
+
.filter_map(|icon| icon_candidate(root, icon, platform))
|
|
1456
|
+
.collect::<Vec<_>>();
|
|
1457
|
+
let source = if platform == "darwin" {
|
|
1458
|
+
candidates
|
|
1459
|
+
.iter()
|
|
1460
|
+
.find(|candidate| candidate.exists() && path_extension(candidate) == Some("icns"))
|
|
1461
|
+
.cloned()
|
|
1462
|
+
.or_else(|| {
|
|
1463
|
+
candidates
|
|
1464
|
+
.iter()
|
|
1465
|
+
.find(|candidate| candidate.exists())
|
|
1466
|
+
.cloned()
|
|
1467
|
+
})
|
|
1468
|
+
} else {
|
|
1469
|
+
candidates
|
|
1470
|
+
.iter()
|
|
1471
|
+
.find(|candidate| candidate.exists())
|
|
1472
|
+
.cloned()
|
|
1473
|
+
};
|
|
1474
|
+
let Some(source) = source else {
|
|
1475
|
+
if let Some(first) = configured_icons.first() {
|
|
1476
|
+
let expected = icon_candidate(root, first, platform)
|
|
1477
|
+
.unwrap_or_else(|| resolve_project_path(root, first));
|
|
1478
|
+
warnings.push(format!(
|
|
1479
|
+
"Configured icon was not found for {platform}: {}.",
|
|
1480
|
+
expected.display()
|
|
1481
|
+
));
|
|
1482
|
+
}
|
|
1483
|
+
return Ok(None);
|
|
1484
|
+
};
|
|
1485
|
+
|
|
1486
|
+
if platform == "darwin" && path_extension(&source) == Some("icon") {
|
|
1487
|
+
warnings.push(
|
|
1488
|
+
"macOS .icon files are not applied yet; provide an .icns icon for now.".to_string(),
|
|
1489
|
+
);
|
|
1490
|
+
return Ok(None);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
if platform == "win32" {
|
|
1494
|
+
warnings.push("Windows executable icon embedding is not implemented yet.".to_string());
|
|
1495
|
+
return Ok(None);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
if platform == "linux" {
|
|
1499
|
+
warnings.push(
|
|
1500
|
+
"Linux executable icons are not embedded; set the BrowserWindow icon in app code."
|
|
1501
|
+
.to_string(),
|
|
1502
|
+
);
|
|
1503
|
+
return Ok(None);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
let extension = path_extension(&source).unwrap_or("icns");
|
|
1507
|
+
let destination = app_resources_dir.join(format!("{artifact_name}.{extension}"));
|
|
1508
|
+
|
|
1509
|
+
Ok(Some(IconResource {
|
|
1510
|
+
from: utf8_path(source)?,
|
|
1511
|
+
to: utf8_path(destination)?,
|
|
1512
|
+
}))
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
fn path_extension(path: &Path) -> Option<&str> {
|
|
1516
|
+
path.extension().and_then(|extension| extension.to_str())
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
fn icon_candidate(root: &Path, configured_icon: &str, platform: &str) -> Option<PathBuf> {
|
|
1520
|
+
if configured_icon.trim().is_empty() {
|
|
1521
|
+
return None;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
let path = resolve_project_path(root, configured_icon);
|
|
1525
|
+
if path.extension().is_some() {
|
|
1526
|
+
return Some(path);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
let extension = match platform {
|
|
1530
|
+
"darwin" => "icns",
|
|
1531
|
+
"win32" => "ico",
|
|
1532
|
+
"linux" => "png",
|
|
1533
|
+
_ => return Some(path),
|
|
1534
|
+
};
|
|
1535
|
+
Some(path.with_extension(extension))
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
fn resolve_extra_resources(
|
|
1539
|
+
root: &Path,
|
|
1540
|
+
extra_resources: &[String],
|
|
1541
|
+
app_resources_dir: &Path,
|
|
1542
|
+
warnings: &mut Vec<String>,
|
|
1543
|
+
) -> Result<Vec<CopyStep>> {
|
|
1544
|
+
extra_resources
|
|
1545
|
+
.iter()
|
|
1546
|
+
.filter(|resource| !resource.trim().is_empty())
|
|
1547
|
+
.map(|resource| {
|
|
1548
|
+
let source = resolve_project_path(root, resource);
|
|
1549
|
+
if !source.exists() {
|
|
1550
|
+
warnings.push(format!(
|
|
1551
|
+
"Configured extra resource does not exist and packaging will fail: {}.",
|
|
1552
|
+
source.display()
|
|
1553
|
+
));
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
let file_name = source
|
|
1557
|
+
.file_name()
|
|
1558
|
+
.with_context(|| format!("Extra resource has no file name: {}", source.display()))?
|
|
1559
|
+
.to_owned();
|
|
1560
|
+
Ok(CopyStep {
|
|
1561
|
+
from: utf8_path(source)?,
|
|
1562
|
+
to: utf8_path(app_resources_dir.join(file_name))?,
|
|
1563
|
+
})
|
|
1564
|
+
})
|
|
1565
|
+
.collect()
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
fn resolve_project_path(root: &Path, path: &str) -> PathBuf {
|
|
1569
|
+
let path = Path::new(path);
|
|
1570
|
+
if path.is_absolute() {
|
|
1571
|
+
path.to_path_buf()
|
|
1572
|
+
} else {
|
|
1573
|
+
root.join(path)
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
fn default_bundle_identifier(artifact_name: &str) -> String {
|
|
1578
|
+
let component = artifact_name
|
|
1579
|
+
.chars()
|
|
1580
|
+
.map(|char| {
|
|
1581
|
+
if char.is_ascii_alphanumeric() || char == '-' {
|
|
1582
|
+
char
|
|
1583
|
+
} else {
|
|
1584
|
+
'.'
|
|
1585
|
+
}
|
|
1586
|
+
})
|
|
1587
|
+
.collect::<String>()
|
|
1588
|
+
.trim_matches(['.', '-'])
|
|
1589
|
+
.to_string();
|
|
1590
|
+
let component = if component.is_empty() {
|
|
1591
|
+
"electron-app".to_string()
|
|
1592
|
+
} else {
|
|
1593
|
+
component
|
|
1594
|
+
};
|
|
1595
|
+
format!("com.electron.{component}")
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
fn copy_project_files(source: &Path, destination: &Path, output_dir: &Path) -> Result<()> {
|
|
1599
|
+
for entry in
|
|
1600
|
+
fs::read_dir(source).with_context(|| format!("Could not read {}", source.display()))?
|
|
1601
|
+
{
|
|
1602
|
+
let entry = entry?;
|
|
1603
|
+
let source_path = entry.path();
|
|
1604
|
+
let file_name = entry.file_name();
|
|
1605
|
+
let file_name = file_name.to_string_lossy();
|
|
1606
|
+
|
|
1607
|
+
if should_skip_project_entry(&source_path, &file_name, output_dir) {
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
let destination_path = destination.join(file_name.as_ref());
|
|
1612
|
+
if source_path.is_dir() {
|
|
1613
|
+
copy_project_files(&source_path, &destination_path, output_dir)?;
|
|
1614
|
+
} else {
|
|
1615
|
+
if let Some(parent) = destination_path.parent() {
|
|
1616
|
+
fs::create_dir_all(parent)
|
|
1617
|
+
.with_context(|| format!("Could not create {}", parent.display()))?;
|
|
1618
|
+
}
|
|
1619
|
+
fs::copy(&source_path, &destination_path).with_context(|| {
|
|
1620
|
+
format!(
|
|
1621
|
+
"Could not copy {} to {}",
|
|
1622
|
+
source_path.display(),
|
|
1623
|
+
destination_path.display()
|
|
1624
|
+
)
|
|
1625
|
+
})?;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
Ok(())
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
#[derive(Debug)]
|
|
1633
|
+
struct DependencyRequest {
|
|
1634
|
+
name: String,
|
|
1635
|
+
requested_by: Option<PathBuf>,
|
|
1636
|
+
optional: bool,
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
fn copy_runtime_dependencies(
|
|
1640
|
+
root: &Path,
|
|
1641
|
+
app_dir: &Path,
|
|
1642
|
+
snapshot: &ProjectSnapshot,
|
|
1643
|
+
) -> Result<()> {
|
|
1644
|
+
if !has_runtime_dependencies(snapshot) {
|
|
1645
|
+
return Ok(());
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
let root_node_modules = root.join("node_modules");
|
|
1649
|
+
let app_node_modules = app_dir.join("node_modules");
|
|
1650
|
+
let mut queue = VecDeque::new();
|
|
1651
|
+
let mut copied_paths = BTreeSet::new();
|
|
1652
|
+
|
|
1653
|
+
for name in snapshot.dependencies.keys() {
|
|
1654
|
+
queue.push_back(DependencyRequest {
|
|
1655
|
+
name: name.clone(),
|
|
1656
|
+
requested_by: None,
|
|
1657
|
+
optional: false,
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
for name in snapshot.optional_dependencies.keys() {
|
|
1662
|
+
queue.push_back(DependencyRequest {
|
|
1663
|
+
name: name.clone(),
|
|
1664
|
+
requested_by: None,
|
|
1665
|
+
optional: true,
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
while let Some(request) = queue.pop_front() {
|
|
1670
|
+
let Some(package_dir) = resolve_dependency_dir(
|
|
1671
|
+
&root_node_modules,
|
|
1672
|
+
request.requested_by.as_deref(),
|
|
1673
|
+
&request.name,
|
|
1674
|
+
) else {
|
|
1675
|
+
if request.optional {
|
|
1676
|
+
continue;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
bail!(
|
|
1680
|
+
"Runtime dependency '{}' is not installed. Run your package manager install first.",
|
|
1681
|
+
request.name
|
|
1682
|
+
);
|
|
1683
|
+
};
|
|
1684
|
+
|
|
1685
|
+
let canonical_package_dir = package_dir
|
|
1686
|
+
.canonicalize()
|
|
1687
|
+
.with_context(|| format!("Could not resolve {}", package_dir.display()))?;
|
|
1688
|
+
let canonical_root_node_modules = root_node_modules
|
|
1689
|
+
.canonicalize()
|
|
1690
|
+
.with_context(|| format!("Could not resolve {}", root_node_modules.display()))?;
|
|
1691
|
+
if !copied_paths.insert(canonical_package_dir.clone()) {
|
|
1692
|
+
continue;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
let relative_path = canonical_package_dir
|
|
1696
|
+
.strip_prefix(&canonical_root_node_modules)
|
|
1697
|
+
.with_context(|| {
|
|
1698
|
+
format!(
|
|
1699
|
+
"Could not make dependency {} relative to {}",
|
|
1700
|
+
canonical_package_dir.display(),
|
|
1701
|
+
canonical_root_node_modules.display()
|
|
1702
|
+
)
|
|
1703
|
+
})?;
|
|
1704
|
+
let destination = app_node_modules.join(relative_path);
|
|
1705
|
+
copy_recursively(&canonical_package_dir, &destination).with_context(|| {
|
|
1706
|
+
format!(
|
|
1707
|
+
"Could not copy runtime dependency {} to {}",
|
|
1708
|
+
canonical_package_dir.display(),
|
|
1709
|
+
destination.display()
|
|
1710
|
+
)
|
|
1711
|
+
})?;
|
|
1712
|
+
|
|
1713
|
+
let package_json = read_dependency_package_json(&canonical_package_dir)?;
|
|
1714
|
+
for name in string_map(package_json.get("dependencies")).keys() {
|
|
1715
|
+
queue.push_back(DependencyRequest {
|
|
1716
|
+
name: name.clone(),
|
|
1717
|
+
requested_by: Some(canonical_package_dir.clone()),
|
|
1718
|
+
optional: false,
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
for name in string_map(package_json.get("optionalDependencies")).keys() {
|
|
1722
|
+
queue.push_back(DependencyRequest {
|
|
1723
|
+
name: name.clone(),
|
|
1724
|
+
requested_by: Some(canonical_package_dir.clone()),
|
|
1725
|
+
optional: true,
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
Ok(())
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
fn apply_package_metadata(report: &PackageReport) -> Result<()> {
|
|
1734
|
+
if report.platform == "darwin" {
|
|
1735
|
+
apply_macos_metadata(report)?;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
Ok(())
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
fn apply_macos_metadata(report: &PackageReport) -> Result<()> {
|
|
1742
|
+
let bundle_dir = Path::new(report.bundle_dir.as_str());
|
|
1743
|
+
let info_plist_path = bundle_dir.join("Contents/Info.plist");
|
|
1744
|
+
let mut dictionary = if info_plist_path.exists() {
|
|
1745
|
+
match PlistValue::from_file(&info_plist_path)
|
|
1746
|
+
.with_context(|| format!("Could not read {}", info_plist_path.display()))?
|
|
1747
|
+
{
|
|
1748
|
+
PlistValue::Dictionary(dictionary) => dictionary,
|
|
1749
|
+
_ => bail!("{} is not a plist dictionary", info_plist_path.display()),
|
|
1750
|
+
}
|
|
1751
|
+
} else {
|
|
1752
|
+
PlistDictionary::new()
|
|
1753
|
+
};
|
|
1754
|
+
|
|
1755
|
+
set_plist_string(&mut dictionary, "CFBundleName", &report.app_name);
|
|
1756
|
+
set_plist_string(&mut dictionary, "CFBundleDisplayName", &report.app_name);
|
|
1757
|
+
set_plist_string(
|
|
1758
|
+
&mut dictionary,
|
|
1759
|
+
"CFBundleExecutable",
|
|
1760
|
+
&report.executable_name,
|
|
1761
|
+
);
|
|
1762
|
+
set_plist_string(
|
|
1763
|
+
&mut dictionary,
|
|
1764
|
+
"CFBundleIdentifier",
|
|
1765
|
+
&report.metadata.bundle_identifier,
|
|
1766
|
+
);
|
|
1767
|
+
set_plist_string(&mut dictionary, "CFBundlePackageType", "APPL");
|
|
1768
|
+
|
|
1769
|
+
if let Some(version) = &report.metadata.app_version {
|
|
1770
|
+
set_plist_string(&mut dictionary, "CFBundleShortVersionString", version);
|
|
1771
|
+
}
|
|
1772
|
+
if let Some(version) = &report.metadata.build_version {
|
|
1773
|
+
set_plist_string(&mut dictionary, "CFBundleVersion", version);
|
|
1774
|
+
}
|
|
1775
|
+
if let Some(category) = &report.metadata.app_category_type {
|
|
1776
|
+
set_plist_string(&mut dictionary, "LSApplicationCategoryType", category);
|
|
1777
|
+
}
|
|
1778
|
+
if let Some(copyright) = &report.metadata.app_copyright {
|
|
1779
|
+
set_plist_string(&mut dictionary, "NSHumanReadableCopyright", copyright);
|
|
1780
|
+
}
|
|
1781
|
+
if let Some(icon) = &report.metadata.icon {
|
|
1782
|
+
let icon_name = Path::new(icon.to.as_str())
|
|
1783
|
+
.file_name()
|
|
1784
|
+
.and_then(|file_name| file_name.to_str())
|
|
1785
|
+
.context("Icon destination has no file name")?;
|
|
1786
|
+
set_plist_string(&mut dictionary, "CFBundleIconFile", icon_name);
|
|
1787
|
+
}
|
|
1788
|
+
if report.metadata.darwin_dark_mode_support {
|
|
1789
|
+
dictionary.insert(
|
|
1790
|
+
"NSRequiresAquaSystemAppearance".to_string(),
|
|
1791
|
+
PlistValue::Boolean(false),
|
|
1792
|
+
);
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
if let Some(parent) = info_plist_path.parent() {
|
|
1796
|
+
fs::create_dir_all(parent)
|
|
1797
|
+
.with_context(|| format!("Could not create {}", parent.display()))?;
|
|
1798
|
+
}
|
|
1799
|
+
PlistValue::Dictionary(dictionary)
|
|
1800
|
+
.to_file_xml(&info_plist_path)
|
|
1801
|
+
.with_context(|| format!("Could not write {}", info_plist_path.display()))?;
|
|
1802
|
+
|
|
1803
|
+
Ok(())
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
fn set_plist_string(dictionary: &mut PlistDictionary, key: &str, value: &str) {
|
|
1807
|
+
dictionary.insert(key.to_string(), PlistValue::String(value.to_string()));
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
fn copy_package_resources(report: &PackageReport) -> Result<()> {
|
|
1811
|
+
if let Some(icon) = &report.metadata.icon {
|
|
1812
|
+
copy_recursively(Path::new(icon.from.as_str()), Path::new(icon.to.as_str()))
|
|
1813
|
+
.with_context(|| format!("Could not copy icon to {}", icon.to))?;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
for resource in &report.metadata.extra_resources {
|
|
1817
|
+
copy_recursively(
|
|
1818
|
+
Path::new(resource.from.as_str()),
|
|
1819
|
+
Path::new(resource.to.as_str()),
|
|
1820
|
+
)
|
|
1821
|
+
.with_context(|| format!("Could not copy extra resource to {}", resource.to))?;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
Ok(())
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
fn runtime_dependency_warnings(root: &Path, snapshot: &ProjectSnapshot) -> Vec<String> {
|
|
1828
|
+
let mut warnings = Vec::new();
|
|
1829
|
+
let root_node_modules = root.join("node_modules");
|
|
1830
|
+
|
|
1831
|
+
for name in snapshot.dependencies.keys() {
|
|
1832
|
+
if resolve_dependency_dir(&root_node_modules, None, name).is_none() {
|
|
1833
|
+
warnings.push(format!(
|
|
1834
|
+
"Runtime dependency is not installed and packaging will fail: {name}."
|
|
1835
|
+
));
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
for name in snapshot.optional_dependencies.keys() {
|
|
1840
|
+
if resolve_dependency_dir(&root_node_modules, None, name).is_none() {
|
|
1841
|
+
warnings.push(format!(
|
|
1842
|
+
"Optional runtime dependency is not installed and will be skipped: {name}."
|
|
1843
|
+
));
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
warnings
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
fn resolve_dependency_dir(
|
|
1851
|
+
root_node_modules: &Path,
|
|
1852
|
+
requested_by: Option<&Path>,
|
|
1853
|
+
name: &str,
|
|
1854
|
+
) -> Option<PathBuf> {
|
|
1855
|
+
let relative_path = dependency_relative_path(name);
|
|
1856
|
+
|
|
1857
|
+
if let Some(requested_by) = requested_by {
|
|
1858
|
+
let nested = requested_by.join("node_modules").join(&relative_path);
|
|
1859
|
+
if nested.exists() {
|
|
1860
|
+
return Some(nested);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
let hoisted = root_node_modules.join(relative_path);
|
|
1865
|
+
hoisted.exists().then_some(hoisted)
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
fn dependency_relative_path(name: &str) -> PathBuf {
|
|
1869
|
+
let mut path = PathBuf::new();
|
|
1870
|
+
for part in name.split('/') {
|
|
1871
|
+
if !part.is_empty() {
|
|
1872
|
+
path.push(part);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
path
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
fn read_dependency_package_json(package_dir: &Path) -> Result<JsonValue> {
|
|
1879
|
+
let package_json_path = package_dir.join("package.json");
|
|
1880
|
+
let raw = fs::read_to_string(&package_json_path)
|
|
1881
|
+
.with_context(|| format!("Could not read {}", package_json_path.display()))?;
|
|
1882
|
+
serde_json::from_str::<JsonValue>(&raw)
|
|
1883
|
+
.with_context(|| format!("Could not parse {}", package_json_path.display()))
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
fn string_map(value: Option<&JsonValue>) -> BTreeMap<String, String> {
|
|
1887
|
+
value
|
|
1888
|
+
.and_then(JsonValue::as_object)
|
|
1889
|
+
.map(|object| {
|
|
1890
|
+
object
|
|
1891
|
+
.iter()
|
|
1892
|
+
.filter_map(|(key, value)| {
|
|
1893
|
+
value.as_str().map(|value| (key.clone(), value.to_string()))
|
|
1894
|
+
})
|
|
1895
|
+
.collect()
|
|
1896
|
+
})
|
|
1897
|
+
.unwrap_or_default()
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
fn copy_recursively(source: &Path, destination: &Path) -> Result<()> {
|
|
1901
|
+
if source.is_dir() {
|
|
1902
|
+
fs::create_dir_all(destination)
|
|
1903
|
+
.with_context(|| format!("Could not create {}", destination.display()))?;
|
|
1904
|
+
|
|
1905
|
+
for entry in
|
|
1906
|
+
fs::read_dir(source).with_context(|| format!("Could not read {}", source.display()))?
|
|
1907
|
+
{
|
|
1908
|
+
let entry = entry?;
|
|
1909
|
+
let source_path = entry.path();
|
|
1910
|
+
let destination_path = destination.join(entry.file_name());
|
|
1911
|
+
copy_recursively(&source_path, &destination_path)?;
|
|
1912
|
+
}
|
|
1913
|
+
} else {
|
|
1914
|
+
if let Some(parent) = destination.parent() {
|
|
1915
|
+
fs::create_dir_all(parent)
|
|
1916
|
+
.with_context(|| format!("Could not create {}", parent.display()))?;
|
|
1917
|
+
}
|
|
1918
|
+
fs::copy(source, destination).with_context(|| {
|
|
1919
|
+
format!(
|
|
1920
|
+
"Could not copy {} to {}",
|
|
1921
|
+
source.display(),
|
|
1922
|
+
destination.display()
|
|
1923
|
+
)
|
|
1924
|
+
})?;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
Ok(())
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
fn should_skip_project_entry(source_path: &Path, file_name: &str, output_dir: &Path) -> bool {
|
|
1931
|
+
if matches!(file_name, ".git" | "node_modules" | "target") {
|
|
1932
|
+
return true;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
same_path_or_inside(source_path, output_dir)
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
fn same_path_or_inside(path: &Path, parent: &Path) -> bool {
|
|
1939
|
+
match (path.canonicalize(), parent.canonicalize()) {
|
|
1940
|
+
(Ok(path), Ok(parent)) => path == parent || path.starts_with(parent),
|
|
1941
|
+
_ => false,
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
fn rename_runtime_executable(
|
|
1946
|
+
bundle_dir: &Path,
|
|
1947
|
+
executable_name: &str,
|
|
1948
|
+
platform: &str,
|
|
1949
|
+
) -> Result<()> {
|
|
1950
|
+
let current = if platform == "darwin" {
|
|
1951
|
+
bundle_dir.join("Contents/MacOS/Electron")
|
|
1952
|
+
} else if platform == "win32" {
|
|
1953
|
+
bundle_dir.join("electron.exe")
|
|
1954
|
+
} else {
|
|
1955
|
+
bundle_dir.join("electron")
|
|
1956
|
+
};
|
|
1957
|
+
let target = if platform == "darwin" {
|
|
1958
|
+
bundle_dir.join("Contents/MacOS").join(executable_name)
|
|
1959
|
+
} else {
|
|
1960
|
+
bundle_dir.join(executable_name)
|
|
1961
|
+
};
|
|
1962
|
+
|
|
1963
|
+
if current.exists() && current != target {
|
|
1964
|
+
fs::rename(¤t, &target).with_context(|| {
|
|
1965
|
+
format!(
|
|
1966
|
+
"Could not rename {} to {}",
|
|
1967
|
+
current.display(),
|
|
1968
|
+
target.display()
|
|
1969
|
+
)
|
|
1970
|
+
})?;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
Ok(())
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
fn resolve_output_dir(root: &Path, out_dir: &Path) -> PathBuf {
|
|
1977
|
+
if out_dir.is_absolute() {
|
|
1978
|
+
out_dir.to_path_buf()
|
|
1979
|
+
} else {
|
|
1980
|
+
root.join(out_dir)
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
fn electron_source(electron_dist: &Path, platform: &str) -> PathBuf {
|
|
1985
|
+
if platform == "darwin" {
|
|
1986
|
+
electron_dist.join("Electron.app")
|
|
1987
|
+
} else {
|
|
1988
|
+
electron_dist.to_path_buf()
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
fn bundle_dir(package_root: &Path, app_name: &str, platform: &str) -> PathBuf {
|
|
1993
|
+
if platform == "darwin" {
|
|
1994
|
+
package_root.join(format!("{app_name}.app"))
|
|
1995
|
+
} else {
|
|
1996
|
+
package_root.to_path_buf()
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
fn package_root(bundle_dir: &Path, platform: &str) -> PathBuf {
|
|
2001
|
+
if platform == "darwin" {
|
|
2002
|
+
bundle_dir
|
|
2003
|
+
.parent()
|
|
2004
|
+
.expect("macOS bundle should have package parent")
|
|
2005
|
+
.to_path_buf()
|
|
2006
|
+
} else {
|
|
2007
|
+
bundle_dir.to_path_buf()
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
fn app_resources_dir(bundle_dir: &Path, platform: &str) -> PathBuf {
|
|
2012
|
+
if platform == "darwin" {
|
|
2013
|
+
bundle_dir.join("Contents/Resources")
|
|
2014
|
+
} else {
|
|
2015
|
+
bundle_dir.join("resources")
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
fn executable_name(app_name: &str, platform: &str) -> String {
|
|
2020
|
+
let mut name = sanitize_artifact_name(app_name);
|
|
2021
|
+
if platform == "win32" {
|
|
2022
|
+
name.push_str(".exe");
|
|
2023
|
+
}
|
|
2024
|
+
name
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
fn clean_app_name(name: &str) -> String {
|
|
2028
|
+
let cleaned = name
|
|
2029
|
+
.chars()
|
|
2030
|
+
.map(|char| {
|
|
2031
|
+
if char.is_ascii_alphanumeric() || matches!(char, ' ' | '-' | '_' | '.') {
|
|
2032
|
+
char
|
|
2033
|
+
} else {
|
|
2034
|
+
'-'
|
|
2035
|
+
}
|
|
2036
|
+
})
|
|
2037
|
+
.collect::<String>()
|
|
2038
|
+
.trim_matches([' ', '-', '.', '_'])
|
|
2039
|
+
.to_string();
|
|
2040
|
+
|
|
2041
|
+
if cleaned.is_empty() {
|
|
2042
|
+
"electron-app".to_string()
|
|
2043
|
+
} else {
|
|
2044
|
+
cleaned
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
fn sanitize_artifact_name(name: &str) -> String {
|
|
2049
|
+
let sanitized = name
|
|
2050
|
+
.to_ascii_lowercase()
|
|
2051
|
+
.chars()
|
|
2052
|
+
.map(|char| {
|
|
2053
|
+
if char.is_ascii_alphanumeric() || matches!(char, '-' | '_' | '.') {
|
|
2054
|
+
char
|
|
2055
|
+
} else {
|
|
2056
|
+
'-'
|
|
2057
|
+
}
|
|
2058
|
+
})
|
|
2059
|
+
.collect::<String>()
|
|
2060
|
+
.trim_matches(['-', '.', '_'])
|
|
2061
|
+
.to_string();
|
|
2062
|
+
|
|
2063
|
+
if sanitized.is_empty() {
|
|
2064
|
+
"electron-app".to_string()
|
|
2065
|
+
} else {
|
|
2066
|
+
sanitized
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
fn has_runtime_dependencies(snapshot: &ProjectSnapshot) -> bool {
|
|
2071
|
+
!snapshot.dependencies.is_empty() || !snapshot.optional_dependencies.is_empty()
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
fn current_platform() -> String {
|
|
2075
|
+
if cfg!(target_os = "macos") {
|
|
2076
|
+
"darwin".to_string()
|
|
2077
|
+
} else if cfg!(target_os = "windows") {
|
|
2078
|
+
"win32".to_string()
|
|
2079
|
+
} else {
|
|
2080
|
+
"linux".to_string()
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
fn current_arch() -> String {
|
|
2085
|
+
match std::env::consts::ARCH {
|
|
2086
|
+
"aarch64" => "arm64".to_string(),
|
|
2087
|
+
"x86_64" => "x64".to_string(),
|
|
2088
|
+
arch => arch.to_string(),
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
fn utf8_path(path: PathBuf) -> Result<Utf8PathBuf> {
|
|
2093
|
+
Utf8PathBuf::from_path_buf(path).map_err(|path| {
|
|
2094
|
+
anyhow::anyhow!(
|
|
2095
|
+
"Path contains invalid UTF-8 and cannot be represented in JSON: {}",
|
|
2096
|
+
path.display()
|
|
2097
|
+
)
|
|
2098
|
+
})
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
impl PackageStatus {
|
|
2102
|
+
fn as_str(&self) -> &'static str {
|
|
2103
|
+
match self {
|
|
2104
|
+
PackageStatus::Planned => "planned",
|
|
2105
|
+
PackageStatus::Packaged => "packaged",
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
impl PackagerConfig {
|
|
2111
|
+
fn merge(&mut self, other: PackagerConfig) {
|
|
2112
|
+
self.name = other.name.or_else(|| self.name.take());
|
|
2113
|
+
self.executable_name = other
|
|
2114
|
+
.executable_name
|
|
2115
|
+
.or_else(|| self.executable_name.take());
|
|
2116
|
+
self.app_bundle_id = other.app_bundle_id.or_else(|| self.app_bundle_id.take());
|
|
2117
|
+
self.app_category_type = other
|
|
2118
|
+
.app_category_type
|
|
2119
|
+
.or_else(|| self.app_category_type.take());
|
|
2120
|
+
self.app_version = other.app_version.or_else(|| self.app_version.take());
|
|
2121
|
+
self.build_version = other.build_version.or_else(|| self.build_version.take());
|
|
2122
|
+
self.app_copyright = other.app_copyright.or_else(|| self.app_copyright.take());
|
|
2123
|
+
if !other.icon.is_empty() {
|
|
2124
|
+
self.icon = other.icon;
|
|
2125
|
+
}
|
|
2126
|
+
if !other.extra_resource.is_empty() {
|
|
2127
|
+
self.extra_resource = other.extra_resource;
|
|
2128
|
+
}
|
|
2129
|
+
self.darwin_dark_mode_support =
|
|
2130
|
+
other.darwin_dark_mode_support || self.darwin_dark_mode_support;
|
|
2131
|
+
if other.osx_sign.configured {
|
|
2132
|
+
self.osx_sign = other.osx_sign;
|
|
2133
|
+
}
|
|
2134
|
+
if other.osx_notarize.configured {
|
|
2135
|
+
self.osx_notarize = other.osx_notarize;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
impl PackageReport {
|
|
2141
|
+
pub(crate) fn project(&self) -> &ProjectSnapshot {
|
|
2142
|
+
&self.project
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
pub(crate) fn mark_packaged(&mut self) {
|
|
2146
|
+
self.status = PackageStatus::Packaged;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
pub(crate) fn app_name(&self) -> &str {
|
|
2150
|
+
&self.app_name
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
pub(crate) fn executable_name(&self) -> &str {
|
|
2154
|
+
&self.executable_name
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
pub(crate) fn artifact_stem(&self) -> String {
|
|
2158
|
+
sanitize_artifact_name(&self.app_name)
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
pub(crate) fn platform(&self) -> &str {
|
|
2162
|
+
&self.platform
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
pub(crate) fn arch(&self) -> &str {
|
|
2166
|
+
&self.arch
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
pub(crate) fn output_dir(&self) -> &Utf8PathBuf {
|
|
2170
|
+
&self.output_dir
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
pub(crate) fn bundle_dir(&self) -> &Utf8PathBuf {
|
|
2174
|
+
&self.bundle_dir
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
pub(crate) fn warnings(&self) -> &[String] {
|
|
2178
|
+
&self.warnings
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
#[cfg(test)]
|
|
2183
|
+
mod tests {
|
|
2184
|
+
use super::*;
|
|
2185
|
+
|
|
2186
|
+
#[test]
|
|
2187
|
+
fn plans_package_output_for_current_platform() {
|
|
2188
|
+
let root = unique_temp_dir("plan");
|
|
2189
|
+
write_package_json(&root);
|
|
2190
|
+
write_fake_electron_dist(&root);
|
|
2191
|
+
|
|
2192
|
+
let args = PackageArgs {
|
|
2193
|
+
cwd: root.clone(),
|
|
2194
|
+
out_dir: PathBuf::from("out"),
|
|
2195
|
+
name: None,
|
|
2196
|
+
platform: None,
|
|
2197
|
+
arch: None,
|
|
2198
|
+
force: false,
|
|
2199
|
+
dry_run: true,
|
|
2200
|
+
json: true,
|
|
2201
|
+
};
|
|
2202
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
2203
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
2204
|
+
|
|
2205
|
+
assert_eq!(report.app_name, "starter-app");
|
|
2206
|
+
assert_eq!(report.platform, current_platform());
|
|
2207
|
+
assert_eq!(report.arch, current_arch());
|
|
2208
|
+
assert!(report.warnings.is_empty());
|
|
2209
|
+
|
|
2210
|
+
let _ = fs::remove_dir_all(root);
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
#[test]
|
|
2214
|
+
fn packages_fake_electron_runtime_and_app_files() {
|
|
2215
|
+
let root = unique_temp_dir("execute");
|
|
2216
|
+
write_package_json(&root);
|
|
2217
|
+
write_app_file(&root);
|
|
2218
|
+
write_fake_electron_dist(&root);
|
|
2219
|
+
fs::create_dir_all(root.join("node_modules/ignored"))
|
|
2220
|
+
.expect("node_modules should be created");
|
|
2221
|
+
fs::write(root.join("node_modules/ignored/file.js"), "")
|
|
2222
|
+
.expect("ignored node module should be written");
|
|
2223
|
+
|
|
2224
|
+
let args = PackageArgs {
|
|
2225
|
+
cwd: root.clone(),
|
|
2226
|
+
out_dir: PathBuf::from("out"),
|
|
2227
|
+
name: Some("Starter App".to_string()),
|
|
2228
|
+
platform: None,
|
|
2229
|
+
arch: None,
|
|
2230
|
+
force: false,
|
|
2231
|
+
dry_run: false,
|
|
2232
|
+
json: false,
|
|
2233
|
+
};
|
|
2234
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
2235
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
2236
|
+
|
|
2237
|
+
execute_package(&report, false).expect("package should succeed");
|
|
2238
|
+
|
|
2239
|
+
let app_dir = Path::new(report.app_resources_dir.as_str()).join("app");
|
|
2240
|
+
assert!(app_dir.join("package.json").exists());
|
|
2241
|
+
assert!(app_dir.join("src/main.js").exists());
|
|
2242
|
+
assert!(!app_dir.join("node_modules").exists());
|
|
2243
|
+
|
|
2244
|
+
if current_platform() == "darwin" {
|
|
2245
|
+
assert!(Path::new(report.bundle_dir.as_str())
|
|
2246
|
+
.join("Contents")
|
|
2247
|
+
.exists());
|
|
2248
|
+
} else {
|
|
2249
|
+
assert!(Path::new(report.bundle_dir.as_str())
|
|
2250
|
+
.join(report.executable_name)
|
|
2251
|
+
.exists());
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
let _ = fs::remove_dir_all(root);
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
#[test]
|
|
2258
|
+
fn packages_runtime_dependency_closure_from_node_modules() {
|
|
2259
|
+
let root = unique_temp_dir("runtime-deps");
|
|
2260
|
+
fs::write(
|
|
2261
|
+
root.join("package.json"),
|
|
2262
|
+
r#"{"name":"starter-app","version":"0.1.0","main":"src/main.js","dependencies":{"dep-a":"1.0.0"},"devDependencies":{"electron":"30.0.0","dev-only":"1.0.0"}}"#,
|
|
2263
|
+
)
|
|
2264
|
+
.expect("package.json should be written");
|
|
2265
|
+
write_app_file(&root);
|
|
2266
|
+
write_fake_electron_dist(&root);
|
|
2267
|
+
write_dependency_package(
|
|
2268
|
+
&root,
|
|
2269
|
+
"dep-a",
|
|
2270
|
+
r#"{"name":"dep-a","version":"1.0.0","dependencies":{"dep-b":"1.0.0"}}"#,
|
|
2271
|
+
);
|
|
2272
|
+
write_dependency_package(&root, "dep-b", r#"{"name":"dep-b","version":"1.0.0"}"#);
|
|
2273
|
+
write_dependency_package(
|
|
2274
|
+
&root,
|
|
2275
|
+
"dev-only",
|
|
2276
|
+
r#"{"name":"dev-only","version":"1.0.0"}"#,
|
|
2277
|
+
);
|
|
2278
|
+
|
|
2279
|
+
let args = PackageArgs {
|
|
2280
|
+
cwd: root.clone(),
|
|
2281
|
+
out_dir: PathBuf::from("out"),
|
|
2282
|
+
name: None,
|
|
2283
|
+
platform: None,
|
|
2284
|
+
arch: None,
|
|
2285
|
+
force: false,
|
|
2286
|
+
dry_run: false,
|
|
2287
|
+
json: false,
|
|
2288
|
+
};
|
|
2289
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
2290
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
2291
|
+
|
|
2292
|
+
assert!(report.warnings.is_empty());
|
|
2293
|
+
execute_package(&report, false).expect("package should succeed");
|
|
2294
|
+
|
|
2295
|
+
let app_node_modules = Path::new(report.app_resources_dir.as_str())
|
|
2296
|
+
.join("app")
|
|
2297
|
+
.join("node_modules");
|
|
2298
|
+
assert!(app_node_modules.join("dep-a/package.json").exists());
|
|
2299
|
+
assert!(app_node_modules.join("dep-b/package.json").exists());
|
|
2300
|
+
assert!(!app_node_modules.join("dev-only").exists());
|
|
2301
|
+
|
|
2302
|
+
let _ = fs::remove_dir_all(root);
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
#[test]
|
|
2306
|
+
fn plans_packager_metadata_from_package_json() {
|
|
2307
|
+
let root = unique_temp_dir("metadata-plan");
|
|
2308
|
+
write_metadata_package_json(&root);
|
|
2309
|
+
write_app_file(&root);
|
|
2310
|
+
write_fake_electron_dist(&root);
|
|
2311
|
+
write_icon_and_resource_files(&root);
|
|
2312
|
+
|
|
2313
|
+
let args = PackageArgs {
|
|
2314
|
+
cwd: root.clone(),
|
|
2315
|
+
out_dir: PathBuf::from("out"),
|
|
2316
|
+
name: None,
|
|
2317
|
+
platform: None,
|
|
2318
|
+
arch: None,
|
|
2319
|
+
force: false,
|
|
2320
|
+
dry_run: true,
|
|
2321
|
+
json: true,
|
|
2322
|
+
};
|
|
2323
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
2324
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
2325
|
+
|
|
2326
|
+
assert_eq!(report.app_name, "Starter Pro");
|
|
2327
|
+
assert_eq!(
|
|
2328
|
+
report.executable_name,
|
|
2329
|
+
executable_name("StarterExec", &report.platform)
|
|
2330
|
+
);
|
|
2331
|
+
assert_eq!(report.metadata.bundle_identifier, "com.example.starter");
|
|
2332
|
+
assert_eq!(report.metadata.app_version.as_deref(), Some("2.3.4"));
|
|
2333
|
+
assert_eq!(report.metadata.build_version.as_deref(), Some("234"));
|
|
2334
|
+
assert_eq!(
|
|
2335
|
+
report.metadata.app_category_type.as_deref(),
|
|
2336
|
+
Some("public.app-category.developer-tools")
|
|
2337
|
+
);
|
|
2338
|
+
assert_eq!(
|
|
2339
|
+
report.metadata.app_copyright.as_deref(),
|
|
2340
|
+
Some("Copyright 2026 Example")
|
|
2341
|
+
);
|
|
2342
|
+
assert_eq!(report.metadata.extra_resources.len(), 1);
|
|
2343
|
+
assert!(report
|
|
2344
|
+
.copy_steps
|
|
2345
|
+
.iter()
|
|
2346
|
+
.any(|step| step.to.as_str().ends_with("config.json")));
|
|
2347
|
+
|
|
2348
|
+
if current_platform() == "darwin" {
|
|
2349
|
+
assert!(report.metadata.icon.is_some());
|
|
2350
|
+
assert!(report.warnings.is_empty());
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
let _ = fs::remove_dir_all(root);
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
#[test]
|
|
2357
|
+
fn plans_packager_metadata_from_forge_config_js() {
|
|
2358
|
+
let root = unique_temp_dir("forge-config-metadata");
|
|
2359
|
+
write_package_json(&root);
|
|
2360
|
+
fs::write(
|
|
2361
|
+
root.join("forge.config.js"),
|
|
2362
|
+
r#"
|
|
2363
|
+
module.exports = {
|
|
2364
|
+
packagerConfig: {
|
|
2365
|
+
name: 'Forge Config App',
|
|
2366
|
+
executableName: 'ForgeExec',
|
|
2367
|
+
appBundleId: 'com.example.forge-config',
|
|
2368
|
+
},
|
|
2369
|
+
};
|
|
2370
|
+
"#,
|
|
2371
|
+
)
|
|
2372
|
+
.expect("forge config should be written");
|
|
2373
|
+
write_app_file(&root);
|
|
2374
|
+
write_fake_electron_dist(&root);
|
|
2375
|
+
|
|
2376
|
+
let args = PackageArgs {
|
|
2377
|
+
cwd: root.clone(),
|
|
2378
|
+
out_dir: PathBuf::from("out"),
|
|
2379
|
+
name: None,
|
|
2380
|
+
platform: None,
|
|
2381
|
+
arch: None,
|
|
2382
|
+
force: false,
|
|
2383
|
+
dry_run: true,
|
|
2384
|
+
json: true,
|
|
2385
|
+
};
|
|
2386
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
2387
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
2388
|
+
|
|
2389
|
+
assert_eq!(report.app_name, "Forge Config App");
|
|
2390
|
+
assert_eq!(
|
|
2391
|
+
report.executable_name,
|
|
2392
|
+
executable_name("ForgeExec", &report.platform)
|
|
2393
|
+
);
|
|
2394
|
+
assert_eq!(
|
|
2395
|
+
report.metadata.bundle_identifier,
|
|
2396
|
+
"com.example.forge-config"
|
|
2397
|
+
);
|
|
2398
|
+
|
|
2399
|
+
let _ = fs::remove_dir_all(root);
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
#[test]
|
|
2403
|
+
fn plans_macos_signing_and_notarization_without_serializing_secrets() {
|
|
2404
|
+
let root = unique_temp_dir("macos-signing-plan");
|
|
2405
|
+
write_package_json(&root);
|
|
2406
|
+
fs::write(root.join("entitlements.plist"), "<plist></plist>")
|
|
2407
|
+
.expect("entitlements should be written");
|
|
2408
|
+
fs::write(root.join("AuthKey_TEST.p8"), "secret api key")
|
|
2409
|
+
.expect("api key should be written");
|
|
2410
|
+
fs::write(
|
|
2411
|
+
root.join("forge.config.js"),
|
|
2412
|
+
r#"
|
|
2413
|
+
module.exports = {
|
|
2414
|
+
packagerConfig: {
|
|
2415
|
+
osxSign: {
|
|
2416
|
+
identity: 'Developer ID Application: Example, Inc. (TEAMID1234)',
|
|
2417
|
+
entitlements: 'entitlements.plist',
|
|
2418
|
+
entitlementsInherit: 'entitlements.plist',
|
|
2419
|
+
hardenedRuntime: true,
|
|
2420
|
+
gatekeeperAssess: false,
|
|
2421
|
+
},
|
|
2422
|
+
osxNotarize: {
|
|
2423
|
+
appleApiKey: 'AuthKey_TEST.p8',
|
|
2424
|
+
appleApiKeyId: 'SECRET_KEY_ID',
|
|
2425
|
+
appleApiIssuer: 'SECRET_ISSUER_ID',
|
|
2426
|
+
},
|
|
2427
|
+
},
|
|
2428
|
+
};
|
|
2429
|
+
"#,
|
|
2430
|
+
)
|
|
2431
|
+
.expect("forge config should be written");
|
|
2432
|
+
write_app_file(&root);
|
|
2433
|
+
write_fake_electron_dist(&root);
|
|
2434
|
+
|
|
2435
|
+
let args = PackageArgs {
|
|
2436
|
+
cwd: root.clone(),
|
|
2437
|
+
out_dir: PathBuf::from("out"),
|
|
2438
|
+
name: None,
|
|
2439
|
+
platform: Some("darwin".to_string()),
|
|
2440
|
+
arch: Some("arm64".to_string()),
|
|
2441
|
+
force: false,
|
|
2442
|
+
dry_run: true,
|
|
2443
|
+
json: true,
|
|
2444
|
+
};
|
|
2445
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
2446
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
2447
|
+
|
|
2448
|
+
assert!(report.signing.macos.sign.configured);
|
|
2449
|
+
assert!(report.signing.macos.sign.enabled);
|
|
2450
|
+
assert!(!report.signing.macos.sign.will_execute);
|
|
2451
|
+
assert_eq!(
|
|
2452
|
+
report.signing.macos.sign.method.as_deref(),
|
|
2453
|
+
Some("certificate-identity")
|
|
2454
|
+
);
|
|
2455
|
+
assert_eq!(
|
|
2456
|
+
report.signing.macos.sign.identity.as_deref(),
|
|
2457
|
+
Some("Developer ID Application: Example, Inc. (TEAMID1234)")
|
|
2458
|
+
);
|
|
2459
|
+
assert_eq!(report.signing.macos.sign.hardened_runtime, Some(true));
|
|
2460
|
+
assert_eq!(report.signing.macos.sign.gatekeeper_assess, Some(false));
|
|
2461
|
+
assert_eq!(report.signing.macos.sign.entitlements.len(), 2);
|
|
2462
|
+
assert!(report.signing.macos.notarize.configured);
|
|
2463
|
+
assert!(!report.signing.macos.notarize.will_execute);
|
|
2464
|
+
assert_eq!(
|
|
2465
|
+
report.signing.macos.notarize.auth_method.as_deref(),
|
|
2466
|
+
Some("app-store-connect-api-key")
|
|
2467
|
+
);
|
|
2468
|
+
assert!(report.signing.macos.notarize.apple_api_key.is_some());
|
|
2469
|
+
assert!(report
|
|
2470
|
+
.warnings
|
|
2471
|
+
.iter()
|
|
2472
|
+
.any(|warning| warning.contains("Rust-native keychain identity signing")));
|
|
2473
|
+
assert!(report
|
|
2474
|
+
.warnings
|
|
2475
|
+
.iter()
|
|
2476
|
+
.any(|warning| warning.contains("p12File Developer ID signing")));
|
|
2477
|
+
|
|
2478
|
+
let json = serde_json::to_string(&report).expect("report should serialize");
|
|
2479
|
+
assert!(!json.contains("SECRET_KEY_ID"));
|
|
2480
|
+
assert!(!json.contains("SECRET_ISSUER_ID"));
|
|
2481
|
+
assert!(!json.contains("secret api key"));
|
|
2482
|
+
|
|
2483
|
+
let _ = fs::remove_dir_all(root);
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
#[test]
|
|
2487
|
+
fn plans_macos_ad_hoc_signing_execution() {
|
|
2488
|
+
let root = unique_temp_dir("macos-ad-hoc-signing-plan");
|
|
2489
|
+
write_package_json(&root);
|
|
2490
|
+
fs::write(
|
|
2491
|
+
root.join("forge.config.js"),
|
|
2492
|
+
r#"
|
|
2493
|
+
module.exports = {
|
|
2494
|
+
packagerConfig: {
|
|
2495
|
+
osxSign: {
|
|
2496
|
+
identity: '-',
|
|
2497
|
+
hardenedRuntime: true,
|
|
2498
|
+
},
|
|
2499
|
+
},
|
|
2500
|
+
};
|
|
2501
|
+
"#,
|
|
2502
|
+
)
|
|
2503
|
+
.expect("forge config should be written");
|
|
2504
|
+
write_app_file(&root);
|
|
2505
|
+
write_fake_electron_dist(&root);
|
|
2506
|
+
|
|
2507
|
+
let args = PackageArgs {
|
|
2508
|
+
cwd: root.clone(),
|
|
2509
|
+
out_dir: PathBuf::from("out"),
|
|
2510
|
+
name: None,
|
|
2511
|
+
platform: Some("darwin".to_string()),
|
|
2512
|
+
arch: Some("arm64".to_string()),
|
|
2513
|
+
force: false,
|
|
2514
|
+
dry_run: true,
|
|
2515
|
+
json: true,
|
|
2516
|
+
};
|
|
2517
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
2518
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
2519
|
+
|
|
2520
|
+
assert!(report.signing.macos.sign.configured);
|
|
2521
|
+
assert!(report.signing.macos.sign.enabled);
|
|
2522
|
+
assert!(report.signing.macos.sign.will_execute);
|
|
2523
|
+
assert_eq!(report.signing.macos.sign.method.as_deref(), Some("ad-hoc"));
|
|
2524
|
+
assert_eq!(report.signing.macos.sign.identity.as_deref(), Some("-"));
|
|
2525
|
+
assert_eq!(report.signing.macos.sign.hardened_runtime, Some(true));
|
|
2526
|
+
assert!(!report.warnings.iter().any(|warning| {
|
|
2527
|
+
warning.contains("Rust-native keychain identity signing")
|
|
2528
|
+
|| warning.contains("Rust-native signing is not implemented")
|
|
2529
|
+
}));
|
|
2530
|
+
|
|
2531
|
+
let _ = fs::remove_dir_all(root);
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
#[test]
|
|
2535
|
+
fn plans_macos_p12_signing_without_serializing_password() {
|
|
2536
|
+
let root = unique_temp_dir("macos-p12-signing-plan");
|
|
2537
|
+
write_package_json(&root);
|
|
2538
|
+
fs::write(root.join("developer-id.p12"), "not a real p12")
|
|
2539
|
+
.expect("p12 placeholder should be written");
|
|
2540
|
+
fs::write(
|
|
2541
|
+
root.join("forge.config.js"),
|
|
2542
|
+
r#"
|
|
2543
|
+
module.exports = {
|
|
2544
|
+
packagerConfig: {
|
|
2545
|
+
osxSign: {
|
|
2546
|
+
identity: 'Developer ID Application: Example, Inc. (TEAMID1234)',
|
|
2547
|
+
p12File: 'developer-id.p12',
|
|
2548
|
+
p12Password: 'p12-secret',
|
|
2549
|
+
timestamp: 'http://timestamp.example.test/tsa',
|
|
2550
|
+
hardenedRuntime: true,
|
|
2551
|
+
},
|
|
2552
|
+
},
|
|
2553
|
+
};
|
|
2554
|
+
"#,
|
|
2555
|
+
)
|
|
2556
|
+
.expect("forge config should be written");
|
|
2557
|
+
write_app_file(&root);
|
|
2558
|
+
write_fake_electron_dist(&root);
|
|
2559
|
+
|
|
2560
|
+
let args = PackageArgs {
|
|
2561
|
+
cwd: root.clone(),
|
|
2562
|
+
out_dir: PathBuf::from("out"),
|
|
2563
|
+
name: None,
|
|
2564
|
+
platform: Some("darwin".to_string()),
|
|
2565
|
+
arch: Some("arm64".to_string()),
|
|
2566
|
+
force: false,
|
|
2567
|
+
dry_run: true,
|
|
2568
|
+
json: true,
|
|
2569
|
+
};
|
|
2570
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
2571
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
2572
|
+
|
|
2573
|
+
assert!(report.signing.macos.sign.configured);
|
|
2574
|
+
assert!(report.signing.macos.sign.enabled);
|
|
2575
|
+
assert!(report.signing.macos.sign.will_execute);
|
|
2576
|
+
assert_eq!(
|
|
2577
|
+
report.signing.macos.sign.method.as_deref(),
|
|
2578
|
+
Some("certificate-p12")
|
|
2579
|
+
);
|
|
2580
|
+
assert_eq!(
|
|
2581
|
+
report.signing.macos.sign.p12_password_source.as_deref(),
|
|
2582
|
+
Some("config")
|
|
2583
|
+
);
|
|
2584
|
+
assert_eq!(
|
|
2585
|
+
report.signing.macos.sign.timestamp_url.as_deref(),
|
|
2586
|
+
Some("http://timestamp.example.test/tsa")
|
|
2587
|
+
);
|
|
2588
|
+
assert!(!report.signing.macos.sign.for_notarization);
|
|
2589
|
+
assert!(report.signing.macos.sign.p12_file.is_some());
|
|
2590
|
+
assert!(report
|
|
2591
|
+
.warnings
|
|
2592
|
+
.iter()
|
|
2593
|
+
.any(|warning| { warning.contains("p12File supplies the signing certificate") }));
|
|
2594
|
+
|
|
2595
|
+
let json = serde_json::to_string(&report).expect("report should serialize");
|
|
2596
|
+
assert!(!json.contains("p12-secret"));
|
|
2597
|
+
|
|
2598
|
+
let _ = fs::remove_dir_all(root);
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
#[test]
|
|
2602
|
+
fn plans_macos_p12_signing_for_notarization_with_default_timestamp() {
|
|
2603
|
+
let root = unique_temp_dir("macos-p12-notarization-signing-plan");
|
|
2604
|
+
write_package_json(&root);
|
|
2605
|
+
fs::write(root.join("developer-id.p12"), "not a real p12")
|
|
2606
|
+
.expect("p12 placeholder should be written");
|
|
2607
|
+
fs::write(
|
|
2608
|
+
root.join("forge.config.js"),
|
|
2609
|
+
r#"
|
|
2610
|
+
module.exports = {
|
|
2611
|
+
packagerConfig: {
|
|
2612
|
+
appBundleId: 'com.example.notarized',
|
|
2613
|
+
osxSign: {
|
|
2614
|
+
p12File: 'developer-id.p12',
|
|
2615
|
+
p12PasswordEnv: 'P12_PASSWORD',
|
|
2616
|
+
},
|
|
2617
|
+
osxNotarize: {
|
|
2618
|
+
keychainProfile: 'notary-profile',
|
|
2619
|
+
},
|
|
2620
|
+
},
|
|
2621
|
+
};
|
|
2622
|
+
"#,
|
|
2623
|
+
)
|
|
2624
|
+
.expect("forge config should be written");
|
|
2625
|
+
write_app_file(&root);
|
|
2626
|
+
write_fake_electron_dist(&root);
|
|
2627
|
+
|
|
2628
|
+
let args = PackageArgs {
|
|
2629
|
+
cwd: root.clone(),
|
|
2630
|
+
out_dir: PathBuf::from("out"),
|
|
2631
|
+
name: None,
|
|
2632
|
+
platform: Some("darwin".to_string()),
|
|
2633
|
+
arch: Some("arm64".to_string()),
|
|
2634
|
+
force: false,
|
|
2635
|
+
dry_run: true,
|
|
2636
|
+
json: true,
|
|
2637
|
+
};
|
|
2638
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
2639
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
2640
|
+
|
|
2641
|
+
assert!(report.signing.macos.sign.will_execute);
|
|
2642
|
+
assert_eq!(
|
|
2643
|
+
report.signing.macos.sign.timestamp_url.as_deref(),
|
|
2644
|
+
Some(APPLE_TIMESTAMP_URL)
|
|
2645
|
+
);
|
|
2646
|
+
assert!(report.signing.macos.sign.for_notarization);
|
|
2647
|
+
assert_eq!(
|
|
2648
|
+
report.signing.macos.notarize.auth_method.as_deref(),
|
|
2649
|
+
Some("keychain-profile")
|
|
2650
|
+
);
|
|
2651
|
+
assert!(!report.signing.macos.notarize.will_execute);
|
|
2652
|
+
assert!(!report
|
|
2653
|
+
.warnings
|
|
2654
|
+
.iter()
|
|
2655
|
+
.any(|warning| warning.contains("ad-hoc signing is not notarizable")));
|
|
2656
|
+
assert!(report
|
|
2657
|
+
.warnings
|
|
2658
|
+
.iter()
|
|
2659
|
+
.any(|warning| warning.contains("requires appleApiKey")));
|
|
2660
|
+
|
|
2661
|
+
let settings = macos_signing_settings(&report).expect("signing settings should build");
|
|
2662
|
+
assert!(settings.for_notarization());
|
|
2663
|
+
assert_eq!(
|
|
2664
|
+
settings.time_stamp_url().map(|url| url.as_str()),
|
|
2665
|
+
Some(APPLE_TIMESTAMP_URL)
|
|
2666
|
+
);
|
|
2667
|
+
|
|
2668
|
+
let json = serde_json::to_string(&report).expect("report should serialize");
|
|
2669
|
+
assert!(!json.contains("P12_PASSWORD="));
|
|
2670
|
+
|
|
2671
|
+
let _ = fs::remove_dir_all(root);
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
#[test]
|
|
2675
|
+
fn plans_macos_native_notarization_execution_with_api_key_auth() {
|
|
2676
|
+
let root = unique_temp_dir("macos-native-notarization-plan");
|
|
2677
|
+
write_package_json(&root);
|
|
2678
|
+
fs::write(root.join("developer-id.p12"), "not a real p12")
|
|
2679
|
+
.expect("p12 placeholder should be written");
|
|
2680
|
+
fs::write(root.join("AuthKey_TEST.p8"), "not a real api key")
|
|
2681
|
+
.expect("api key placeholder should be written");
|
|
2682
|
+
fs::write(
|
|
2683
|
+
root.join("forge.config.js"),
|
|
2684
|
+
r#"
|
|
2685
|
+
module.exports = {
|
|
2686
|
+
packagerConfig: {
|
|
2687
|
+
appBundleId: 'com.example.native-notarized',
|
|
2688
|
+
osxSign: {
|
|
2689
|
+
p12File: 'developer-id.p12',
|
|
2690
|
+
p12Password: 'p12-secret',
|
|
2691
|
+
hardenedRuntime: true,
|
|
2692
|
+
},
|
|
2693
|
+
osxNotarize: {
|
|
2694
|
+
appleApiKey: 'AuthKey_TEST.p8',
|
|
2695
|
+
appleApiKeyId: 'SECRET_KEY_ID',
|
|
2696
|
+
appleApiIssuer: 'SECRET_ISSUER_ID',
|
|
2697
|
+
maxWaitSeconds: 120,
|
|
2698
|
+
},
|
|
2699
|
+
},
|
|
2700
|
+
};
|
|
2701
|
+
"#,
|
|
2702
|
+
)
|
|
2703
|
+
.expect("forge config should be written");
|
|
2704
|
+
write_app_file(&root);
|
|
2705
|
+
write_fake_electron_dist(&root);
|
|
2706
|
+
|
|
2707
|
+
let args = PackageArgs {
|
|
2708
|
+
cwd: root.clone(),
|
|
2709
|
+
out_dir: PathBuf::from("out"),
|
|
2710
|
+
name: None,
|
|
2711
|
+
platform: Some("darwin".to_string()),
|
|
2712
|
+
arch: Some("arm64".to_string()),
|
|
2713
|
+
force: false,
|
|
2714
|
+
dry_run: true,
|
|
2715
|
+
json: true,
|
|
2716
|
+
};
|
|
2717
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
2718
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
2719
|
+
|
|
2720
|
+
assert!(report.signing.macos.sign.for_notarization);
|
|
2721
|
+
assert!(report.signing.macos.notarize.will_execute);
|
|
2722
|
+
assert_eq!(
|
|
2723
|
+
report.signing.macos.notarize.auth_method.as_deref(),
|
|
2724
|
+
Some("app-store-connect-api-key")
|
|
2725
|
+
);
|
|
2726
|
+
assert!(report.signing.macos.notarize.wait);
|
|
2727
|
+
assert_eq!(report.signing.macos.notarize.wait_timeout_seconds, 120);
|
|
2728
|
+
assert!(report.signing.macos.notarize.staple);
|
|
2729
|
+
assert!(!report
|
|
2730
|
+
.warnings
|
|
2731
|
+
.iter()
|
|
2732
|
+
.any(|warning| warning.contains("not implemented")));
|
|
2733
|
+
|
|
2734
|
+
let json = serde_json::to_string(&report).expect("report should serialize");
|
|
2735
|
+
assert!(!json.contains("SECRET_KEY_ID"));
|
|
2736
|
+
assert!(!json.contains("SECRET_ISSUER_ID"));
|
|
2737
|
+
assert!(!json.contains("p12-secret"));
|
|
2738
|
+
assert!(!json.contains("not a real api key"));
|
|
2739
|
+
|
|
2740
|
+
let _ = fs::remove_dir_all(root);
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
#[test]
|
|
2744
|
+
fn warns_when_macos_notarization_timestamp_is_disabled() {
|
|
2745
|
+
let root = unique_temp_dir("macos-p12-notarization-no-timestamp");
|
|
2746
|
+
write_package_json(&root);
|
|
2747
|
+
fs::write(root.join("developer-id.p12"), "not a real p12")
|
|
2748
|
+
.expect("p12 placeholder should be written");
|
|
2749
|
+
fs::write(
|
|
2750
|
+
root.join("forge.config.js"),
|
|
2751
|
+
r#"
|
|
2752
|
+
module.exports = {
|
|
2753
|
+
packagerConfig: {
|
|
2754
|
+
osxSign: {
|
|
2755
|
+
p12File: 'developer-id.p12',
|
|
2756
|
+
timestamp: 'none',
|
|
2757
|
+
},
|
|
2758
|
+
osxNotarize: {
|
|
2759
|
+
keychainProfile: 'notary-profile',
|
|
2760
|
+
},
|
|
2761
|
+
},
|
|
2762
|
+
};
|
|
2763
|
+
"#,
|
|
2764
|
+
)
|
|
2765
|
+
.expect("forge config should be written");
|
|
2766
|
+
write_app_file(&root);
|
|
2767
|
+
write_fake_electron_dist(&root);
|
|
2768
|
+
|
|
2769
|
+
let args = PackageArgs {
|
|
2770
|
+
cwd: root.clone(),
|
|
2771
|
+
out_dir: PathBuf::from("out"),
|
|
2772
|
+
name: None,
|
|
2773
|
+
platform: Some("darwin".to_string()),
|
|
2774
|
+
arch: Some("arm64".to_string()),
|
|
2775
|
+
force: false,
|
|
2776
|
+
dry_run: true,
|
|
2777
|
+
json: true,
|
|
2778
|
+
};
|
|
2779
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
2780
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
2781
|
+
|
|
2782
|
+
assert!(report.signing.macos.sign.will_execute);
|
|
2783
|
+
assert!(report.signing.macos.sign.timestamp_url.is_none());
|
|
2784
|
+
assert!(!report.signing.macos.sign.for_notarization);
|
|
2785
|
+
assert!(report
|
|
2786
|
+
.warnings
|
|
2787
|
+
.iter()
|
|
2788
|
+
.any(|warning| warning.contains("requires a secure timestamp")));
|
|
2789
|
+
|
|
2790
|
+
let _ = fs::remove_dir_all(root);
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
#[test]
|
|
2794
|
+
fn warns_when_macos_notarization_is_configured_without_signing() {
|
|
2795
|
+
let root = unique_temp_dir("notarize-without-sign");
|
|
2796
|
+
write_package_json(&root);
|
|
2797
|
+
fs::write(
|
|
2798
|
+
root.join("forge.config.js"),
|
|
2799
|
+
r#"
|
|
2800
|
+
module.exports = {
|
|
2801
|
+
packagerConfig: {
|
|
2802
|
+
osxSign: false,
|
|
2803
|
+
osxNotarize: {
|
|
2804
|
+
keychainProfile: 'notary-profile',
|
|
2805
|
+
},
|
|
2806
|
+
},
|
|
2807
|
+
};
|
|
2808
|
+
"#,
|
|
2809
|
+
)
|
|
2810
|
+
.expect("forge config should be written");
|
|
2811
|
+
write_app_file(&root);
|
|
2812
|
+
write_fake_electron_dist(&root);
|
|
2813
|
+
|
|
2814
|
+
let args = PackageArgs {
|
|
2815
|
+
cwd: root.clone(),
|
|
2816
|
+
out_dir: PathBuf::from("out"),
|
|
2817
|
+
name: None,
|
|
2818
|
+
platform: Some("darwin".to_string()),
|
|
2819
|
+
arch: Some("arm64".to_string()),
|
|
2820
|
+
force: false,
|
|
2821
|
+
dry_run: true,
|
|
2822
|
+
json: true,
|
|
2823
|
+
};
|
|
2824
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
2825
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
2826
|
+
|
|
2827
|
+
assert!(report.signing.macos.sign.configured);
|
|
2828
|
+
assert!(!report.signing.macos.sign.enabled);
|
|
2829
|
+
assert_eq!(
|
|
2830
|
+
report.signing.macos.notarize.auth_method.as_deref(),
|
|
2831
|
+
Some("keychain-profile")
|
|
2832
|
+
);
|
|
2833
|
+
assert!(report.warnings.iter().any(|warning| {
|
|
2834
|
+
warning.contains("macOS notarization requires packagerConfig.osxSign")
|
|
2835
|
+
}));
|
|
2836
|
+
|
|
2837
|
+
let _ = fs::remove_dir_all(root);
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
#[test]
|
|
2841
|
+
fn packages_macos_info_plist_metadata() {
|
|
2842
|
+
if current_platform() != "darwin" {
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
let root = unique_temp_dir("metadata-execute");
|
|
2847
|
+
write_metadata_package_json(&root);
|
|
2848
|
+
write_app_file(&root);
|
|
2849
|
+
write_fake_electron_dist(&root);
|
|
2850
|
+
write_icon_and_resource_files(&root);
|
|
2851
|
+
|
|
2852
|
+
let args = PackageArgs {
|
|
2853
|
+
cwd: root.clone(),
|
|
2854
|
+
out_dir: PathBuf::from("out"),
|
|
2855
|
+
name: None,
|
|
2856
|
+
platform: None,
|
|
2857
|
+
arch: None,
|
|
2858
|
+
force: false,
|
|
2859
|
+
dry_run: false,
|
|
2860
|
+
json: false,
|
|
2861
|
+
};
|
|
2862
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
2863
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
2864
|
+
|
|
2865
|
+
execute_package(&report, false).expect("package should succeed");
|
|
2866
|
+
|
|
2867
|
+
let bundle_dir = Path::new(report.bundle_dir.as_str());
|
|
2868
|
+
assert!(bundle_dir
|
|
2869
|
+
.join("Contents/MacOS")
|
|
2870
|
+
.join(&report.executable_name)
|
|
2871
|
+
.exists());
|
|
2872
|
+
assert!(bundle_dir
|
|
2873
|
+
.join("Contents/Resources/starter-pro.icns")
|
|
2874
|
+
.exists());
|
|
2875
|
+
assert!(bundle_dir.join("Contents/Resources/config.json").exists());
|
|
2876
|
+
|
|
2877
|
+
let plist = PlistValue::from_file(bundle_dir.join("Contents/Info.plist"))
|
|
2878
|
+
.expect("Info.plist should parse");
|
|
2879
|
+
let dictionary = plist
|
|
2880
|
+
.as_dictionary()
|
|
2881
|
+
.expect("Info.plist should be a dictionary");
|
|
2882
|
+
|
|
2883
|
+
assert_eq!(
|
|
2884
|
+
plist_string(dictionary, "CFBundleDisplayName"),
|
|
2885
|
+
Some("Starter Pro")
|
|
2886
|
+
);
|
|
2887
|
+
assert_eq!(
|
|
2888
|
+
plist_string(dictionary, "CFBundleExecutable"),
|
|
2889
|
+
Some(report.executable_name.as_str())
|
|
2890
|
+
);
|
|
2891
|
+
assert_eq!(
|
|
2892
|
+
plist_string(dictionary, "CFBundleIdentifier"),
|
|
2893
|
+
Some("com.example.starter")
|
|
2894
|
+
);
|
|
2895
|
+
assert_eq!(
|
|
2896
|
+
plist_string(dictionary, "CFBundlePackageType"),
|
|
2897
|
+
Some("APPL")
|
|
2898
|
+
);
|
|
2899
|
+
assert_eq!(
|
|
2900
|
+
plist_string(dictionary, "CFBundleShortVersionString"),
|
|
2901
|
+
Some("2.3.4")
|
|
2902
|
+
);
|
|
2903
|
+
assert_eq!(plist_string(dictionary, "CFBundleVersion"), Some("234"));
|
|
2904
|
+
assert_eq!(
|
|
2905
|
+
plist_string(dictionary, "LSApplicationCategoryType"),
|
|
2906
|
+
Some("public.app-category.developer-tools")
|
|
2907
|
+
);
|
|
2908
|
+
assert_eq!(
|
|
2909
|
+
plist_string(dictionary, "CFBundleIconFile"),
|
|
2910
|
+
Some("starter-pro.icns")
|
|
2911
|
+
);
|
|
2912
|
+
assert_eq!(
|
|
2913
|
+
dictionary
|
|
2914
|
+
.get("NSRequiresAquaSystemAppearance")
|
|
2915
|
+
.and_then(PlistValue::as_boolean),
|
|
2916
|
+
Some(false)
|
|
2917
|
+
);
|
|
2918
|
+
|
|
2919
|
+
let _ = fs::remove_dir_all(root);
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
#[test]
|
|
2923
|
+
fn packages_macos_bundle_with_ad_hoc_signature() {
|
|
2924
|
+
if current_platform() != "darwin" {
|
|
2925
|
+
return;
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
let root = unique_temp_dir("macos-ad-hoc-signing-execute");
|
|
2929
|
+
fs::write(
|
|
2930
|
+
root.join("package.json"),
|
|
2931
|
+
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}}}"#,
|
|
2932
|
+
)
|
|
2933
|
+
.expect("package.json should be written");
|
|
2934
|
+
write_app_file(&root);
|
|
2935
|
+
write_macho_electron_dist(&root);
|
|
2936
|
+
|
|
2937
|
+
let args = PackageArgs {
|
|
2938
|
+
cwd: root.clone(),
|
|
2939
|
+
out_dir: PathBuf::from("out"),
|
|
2940
|
+
name: None,
|
|
2941
|
+
platform: None,
|
|
2942
|
+
arch: None,
|
|
2943
|
+
force: false,
|
|
2944
|
+
dry_run: false,
|
|
2945
|
+
json: false,
|
|
2946
|
+
};
|
|
2947
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
2948
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
2949
|
+
|
|
2950
|
+
assert!(report.signing.macos.sign.will_execute);
|
|
2951
|
+
assert_eq!(report.signing.macos.sign.method.as_deref(), Some("ad-hoc"));
|
|
2952
|
+
assert!(report.warnings.is_empty());
|
|
2953
|
+
|
|
2954
|
+
execute_package(&report, false).expect("package should succeed");
|
|
2955
|
+
|
|
2956
|
+
let bundle_dir = Path::new(report.bundle_dir.as_str());
|
|
2957
|
+
assert!(bundle_dir
|
|
2958
|
+
.join("Contents/_CodeSignature/CodeResources")
|
|
2959
|
+
.exists());
|
|
2960
|
+
|
|
2961
|
+
let executable = bundle_dir
|
|
2962
|
+
.join("Contents/MacOS")
|
|
2963
|
+
.join(&report.executable_name);
|
|
2964
|
+
let executable_data = fs::read(executable).expect("signed executable should read");
|
|
2965
|
+
let macho = apple_codesign::MachFile::parse(&executable_data)
|
|
2966
|
+
.expect("signed executable should parse as Mach-O");
|
|
2967
|
+
assert!(macho.iter_macho().all(|binary| binary
|
|
2968
|
+
.code_signature()
|
|
2969
|
+
.expect("code signature should parse")
|
|
2970
|
+
.is_some()));
|
|
2971
|
+
|
|
2972
|
+
let _ = fs::remove_dir_all(root);
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
#[test]
|
|
2976
|
+
fn packages_macos_bundle_with_p12_certificate_signature() {
|
|
2977
|
+
if current_platform() != "darwin" {
|
|
2978
|
+
return;
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
let Some(p12_fixture) = apple_codesign_test_fixture("apple-codesign-testuser.p12") else {
|
|
2982
|
+
return;
|
|
2983
|
+
};
|
|
2984
|
+
|
|
2985
|
+
let root = unique_temp_dir("macos-p12-signing-execute");
|
|
2986
|
+
fs::copy(&p12_fixture, root.join("developer-id.p12"))
|
|
2987
|
+
.expect("p12 fixture should be copied");
|
|
2988
|
+
fs::write(
|
|
2989
|
+
root.join("package.json"),
|
|
2990
|
+
r#"{"name":"starter-app","version":"0.1.0","main":"src/main.js","devDependencies":{"electron":"30.0.0"},"electronCli":{"packagerConfig":{"appBundleId":"com.example.p12-signed","osxSign":{"p12File":"developer-id.p12","p12Password":"password123","hardenedRuntime":true}}}}"#,
|
|
2991
|
+
)
|
|
2992
|
+
.expect("package.json should be written");
|
|
2993
|
+
write_app_file(&root);
|
|
2994
|
+
write_macho_electron_dist(&root);
|
|
2995
|
+
|
|
2996
|
+
let args = PackageArgs {
|
|
2997
|
+
cwd: root.clone(),
|
|
2998
|
+
out_dir: PathBuf::from("out"),
|
|
2999
|
+
name: None,
|
|
3000
|
+
platform: None,
|
|
3001
|
+
arch: None,
|
|
3002
|
+
force: false,
|
|
3003
|
+
dry_run: false,
|
|
3004
|
+
json: false,
|
|
3005
|
+
};
|
|
3006
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
3007
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
3008
|
+
|
|
3009
|
+
assert!(report.signing.macos.sign.will_execute);
|
|
3010
|
+
assert_eq!(
|
|
3011
|
+
report.signing.macos.sign.method.as_deref(),
|
|
3012
|
+
Some("certificate-p12")
|
|
3013
|
+
);
|
|
3014
|
+
assert!(report.warnings.is_empty());
|
|
3015
|
+
|
|
3016
|
+
execute_package(&report, false).expect("package should succeed");
|
|
3017
|
+
|
|
3018
|
+
let executable = Path::new(report.bundle_dir.as_str())
|
|
3019
|
+
.join("Contents/MacOS")
|
|
3020
|
+
.join(&report.executable_name);
|
|
3021
|
+
let executable_data = fs::read(executable).expect("signed executable should read");
|
|
3022
|
+
let macho = apple_codesign::MachFile::parse(&executable_data)
|
|
3023
|
+
.expect("signed executable should parse as Mach-O");
|
|
3024
|
+
assert!(macho.iter_macho().all(|binary| {
|
|
3025
|
+
let signature = binary
|
|
3026
|
+
.code_signature()
|
|
3027
|
+
.expect("code signature should parse")
|
|
3028
|
+
.expect("code signature should exist");
|
|
3029
|
+
signature
|
|
3030
|
+
.signature_data()
|
|
3031
|
+
.expect("CMS signature should parse")
|
|
3032
|
+
.is_some_and(|data| !data.is_empty())
|
|
3033
|
+
}));
|
|
3034
|
+
|
|
3035
|
+
let _ = fs::remove_dir_all(root);
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
#[test]
|
|
3039
|
+
fn missing_required_runtime_dependency_fails() {
|
|
3040
|
+
let root = unique_temp_dir("runtime-deps");
|
|
3041
|
+
fs::write(
|
|
3042
|
+
root.join("package.json"),
|
|
3043
|
+
r#"{"name":"starter-app","version":"0.1.0","main":"src/main.js","dependencies":{"left-pad":"1.3.0"},"devDependencies":{"electron":"30.0.0"}}"#,
|
|
3044
|
+
)
|
|
3045
|
+
.expect("package.json should be written");
|
|
3046
|
+
write_app_file(&root);
|
|
3047
|
+
write_fake_electron_dist(&root);
|
|
3048
|
+
|
|
3049
|
+
let args = PackageArgs {
|
|
3050
|
+
cwd: root.clone(),
|
|
3051
|
+
out_dir: PathBuf::from("out"),
|
|
3052
|
+
name: None,
|
|
3053
|
+
platform: None,
|
|
3054
|
+
arch: None,
|
|
3055
|
+
force: false,
|
|
3056
|
+
dry_run: false,
|
|
3057
|
+
json: false,
|
|
3058
|
+
};
|
|
3059
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
3060
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
3061
|
+
|
|
3062
|
+
assert!(report.warnings.contains(
|
|
3063
|
+
&"Runtime dependency is not installed and packaging will fail: left-pad.".to_string()
|
|
3064
|
+
));
|
|
3065
|
+
assert!(execute_package(&report, false).is_err());
|
|
3066
|
+
|
|
3067
|
+
let _ = fs::remove_dir_all(root);
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
#[test]
|
|
3071
|
+
fn missing_optional_runtime_dependency_is_skipped() {
|
|
3072
|
+
let root = unique_temp_dir("optional-runtime-deps");
|
|
3073
|
+
fs::write(
|
|
3074
|
+
root.join("package.json"),
|
|
3075
|
+
r#"{"name":"starter-app","version":"0.1.0","main":"src/main.js","optionalDependencies":{"optional-native":"1.0.0"},"devDependencies":{"electron":"30.0.0"}}"#,
|
|
3076
|
+
)
|
|
3077
|
+
.expect("package.json should be written");
|
|
3078
|
+
write_app_file(&root);
|
|
3079
|
+
write_fake_electron_dist(&root);
|
|
3080
|
+
|
|
3081
|
+
let args = PackageArgs {
|
|
3082
|
+
cwd: root.clone(),
|
|
3083
|
+
out_dir: PathBuf::from("out"),
|
|
3084
|
+
name: None,
|
|
3085
|
+
platform: None,
|
|
3086
|
+
arch: None,
|
|
3087
|
+
force: false,
|
|
3088
|
+
dry_run: false,
|
|
3089
|
+
json: false,
|
|
3090
|
+
};
|
|
3091
|
+
let snapshot = crate::project::inspect(&root).expect("project should inspect");
|
|
3092
|
+
let report = build_report(snapshot, &args).expect("report should build");
|
|
3093
|
+
|
|
3094
|
+
assert!(report.warnings.contains(
|
|
3095
|
+
&"Optional runtime dependency is not installed and will be skipped: optional-native."
|
|
3096
|
+
.to_string()
|
|
3097
|
+
));
|
|
3098
|
+
execute_package(&report, false).expect("optional dependency should be skipped");
|
|
3099
|
+
|
|
3100
|
+
let _ = fs::remove_dir_all(root);
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
#[test]
|
|
3104
|
+
fn cleans_scoped_package_names_for_bundle_paths() {
|
|
3105
|
+
assert_eq!(clean_app_name("@scope/app"), "scope-app");
|
|
3106
|
+
assert_eq!(sanitize_artifact_name("Starter App"), "starter-app");
|
|
3107
|
+
assert_eq!(
|
|
3108
|
+
dependency_relative_path("@scope/app"),
|
|
3109
|
+
PathBuf::from("@scope/app")
|
|
3110
|
+
);
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
fn write_package_json(root: &Path) {
|
|
3114
|
+
fs::write(
|
|
3115
|
+
root.join("package.json"),
|
|
3116
|
+
r#"{"name":"starter-app","version":"0.1.0","main":"src/main.js","devDependencies":{"electron":"30.0.0"}}"#,
|
|
3117
|
+
)
|
|
3118
|
+
.expect("package.json should be written");
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
fn write_metadata_package_json(root: &Path) {
|
|
3122
|
+
fs::write(
|
|
3123
|
+
root.join("package.json"),
|
|
3124
|
+
r#"{
|
|
3125
|
+
"name": "starter-app",
|
|
3126
|
+
"productName": "Starter Pro",
|
|
3127
|
+
"version": "2.3.4",
|
|
3128
|
+
"main": "src/main.js",
|
|
3129
|
+
"devDependencies": {
|
|
3130
|
+
"electron": "30.0.0"
|
|
3131
|
+
},
|
|
3132
|
+
"electronCli": {
|
|
3133
|
+
"packagerConfig": {
|
|
3134
|
+
"executableName": "StarterExec",
|
|
3135
|
+
"appBundleId": "com.example.starter",
|
|
3136
|
+
"appCategoryType": "public.app-category.developer-tools",
|
|
3137
|
+
"buildVersion": "234",
|
|
3138
|
+
"appCopyright": "Copyright 2026 Example",
|
|
3139
|
+
"icon": "assets/starter",
|
|
3140
|
+
"extraResource": "assets/config.json",
|
|
3141
|
+
"darwinDarkModeSupport": true
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
}"#,
|
|
3145
|
+
)
|
|
3146
|
+
.expect("package.json should be written");
|
|
3147
|
+
}
|
|
3148
|
+
|
|
3149
|
+
fn write_app_file(root: &Path) {
|
|
3150
|
+
fs::create_dir_all(root.join("src")).expect("src should be created");
|
|
3151
|
+
fs::write(root.join("src/main.js"), "console.log('hello');")
|
|
3152
|
+
.expect("main file should be written");
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
fn write_dependency_package(root: &Path, name: &str, package_json: &str) {
|
|
3156
|
+
let package_dir = root
|
|
3157
|
+
.join("node_modules")
|
|
3158
|
+
.join(dependency_relative_path(name));
|
|
3159
|
+
fs::create_dir_all(&package_dir).expect("dependency package dir should be created");
|
|
3160
|
+
fs::write(package_dir.join("package.json"), package_json)
|
|
3161
|
+
.expect("dependency package.json should be written");
|
|
3162
|
+
fs::write(package_dir.join("index.js"), "module.exports = true;")
|
|
3163
|
+
.expect("dependency index should be written");
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
fn write_icon_and_resource_files(root: &Path) {
|
|
3167
|
+
fs::create_dir_all(root.join("assets")).expect("assets should be created");
|
|
3168
|
+
fs::write(root.join("assets/starter.icns"), b"icns").expect("icon should be written");
|
|
3169
|
+
fs::write(root.join("assets/config.json"), "{}").expect("resource should be written");
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
fn plist_string<'a>(dictionary: &'a PlistDictionary, key: &str) -> Option<&'a str> {
|
|
3173
|
+
dictionary.get(key).and_then(PlistValue::as_string)
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
fn write_fake_electron_dist(root: &Path) {
|
|
3177
|
+
let dist = root.join("node_modules/electron/dist");
|
|
3178
|
+
if current_platform() == "darwin" {
|
|
3179
|
+
let app = dist.join("Electron.app/Contents/MacOS");
|
|
3180
|
+
fs::create_dir_all(&app).expect("fake macOS electron app should be created");
|
|
3181
|
+
fs::write(app.join("Electron"), "").expect("fake macOS binary should be written");
|
|
3182
|
+
} else if current_platform() == "win32" {
|
|
3183
|
+
fs::create_dir_all(&dist).expect("fake electron dist should be created");
|
|
3184
|
+
fs::write(dist.join("electron.exe"), "").expect("fake exe should be written");
|
|
3185
|
+
} else {
|
|
3186
|
+
fs::create_dir_all(&dist).expect("fake electron dist should be created");
|
|
3187
|
+
fs::write(dist.join("electron"), "").expect("fake binary should be written");
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
fn write_macho_electron_dist(root: &Path) {
|
|
3192
|
+
let app = root.join("node_modules/electron/dist/Electron.app/Contents/MacOS");
|
|
3193
|
+
fs::create_dir_all(&app).expect("macOS Electron app should be created");
|
|
3194
|
+
fs::copy(
|
|
3195
|
+
std::env::current_exe().expect("current test executable should resolve"),
|
|
3196
|
+
app.join("Electron"),
|
|
3197
|
+
)
|
|
3198
|
+
.expect("Mach-O test executable should be copied");
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3201
|
+
fn apple_codesign_test_fixture(file_name: &str) -> Option<PathBuf> {
|
|
3202
|
+
let cargo_home = std::env::var_os("CARGO_HOME")
|
|
3203
|
+
.map(PathBuf::from)
|
|
3204
|
+
.or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".cargo")))?;
|
|
3205
|
+
let registry_src = cargo_home.join("registry/src");
|
|
3206
|
+
for index_dir in fs::read_dir(registry_src).ok()? {
|
|
3207
|
+
let index_dir = index_dir.ok()?;
|
|
3208
|
+
for crate_dir in fs::read_dir(index_dir.path()).ok()? {
|
|
3209
|
+
let crate_dir = crate_dir.ok()?;
|
|
3210
|
+
let file_name_matches = crate_dir
|
|
3211
|
+
.file_name()
|
|
3212
|
+
.to_str()
|
|
3213
|
+
.is_some_and(|name| name.starts_with("apple-codesign-"));
|
|
3214
|
+
if file_name_matches {
|
|
3215
|
+
let candidate = crate_dir.path().join("src").join(file_name);
|
|
3216
|
+
if candidate.exists() {
|
|
3217
|
+
return Some(candidate);
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
None
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
fn unique_temp_dir(label: &str) -> PathBuf {
|
|
3227
|
+
let nanos = std::time::SystemTime::now()
|
|
3228
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
3229
|
+
.expect("clock should be after epoch")
|
|
3230
|
+
.as_nanos();
|
|
3231
|
+
let path = std::env::temp_dir().join(format!(
|
|
3232
|
+
"electron-cli-package-{label}-{}-{nanos}",
|
|
3233
|
+
std::process::id()
|
|
3234
|
+
));
|
|
3235
|
+
fs::create_dir_all(&path).expect("temp dir should be created");
|
|
3236
|
+
path
|
|
3237
|
+
}
|
|
3238
|
+
}
|