electron-cli 0.3.0-alpha.11 → 0.3.0-alpha.13
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 +77 -1
- package/Cargo.toml +4 -1
- package/README.md +18 -3
- package/package.json +1 -1
- package/src/cli.rs +5 -3
- package/src/commands/make.rs +1364 -29
- package/src/commands/publish.rs +2 -2
- package/src/project.rs +1 -1
package/src/commands/make.rs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
use std::{
|
|
2
|
+
collections::BTreeMap,
|
|
2
3
|
fs,
|
|
3
4
|
fs::File,
|
|
4
5
|
io::{self, BufWriter, Cursor, Write},
|
|
@@ -6,25 +7,32 @@ use std::{
|
|
|
6
7
|
};
|
|
7
8
|
|
|
8
9
|
use anyhow::{bail, Context, Result};
|
|
10
|
+
use cab::{CabinetBuilder, CompressionType as CabCompressionType};
|
|
9
11
|
use camino::Utf8PathBuf;
|
|
10
12
|
use fatfs::{Dir as FatDir, FileSystem, FormatVolumeOptions, FsOptions, ReadWriteSeek};
|
|
11
13
|
use flate2::{write::GzEncoder, Compression};
|
|
12
14
|
use fscommon::BufStream;
|
|
15
|
+
use msi::{Column, Insert, Language, Package, PackageType, Value};
|
|
13
16
|
use rpm::{BuildConfig, CompressionType, FileOptions, PackageBuilder};
|
|
14
17
|
use serde::Serialize;
|
|
18
|
+
use serde_json::Value as JsonValue;
|
|
15
19
|
use tar::{Builder as TarBuilder, Header as TarHeader};
|
|
20
|
+
use uuid::Uuid;
|
|
16
21
|
use zip::{write::SimpleFileOptions, CompressionMethod, ZipWriter};
|
|
17
22
|
|
|
18
23
|
use crate::{
|
|
19
24
|
cli::{MakeArgs, MakeTarget, PackageArgs},
|
|
20
25
|
commands::package::{self, PackageReport},
|
|
21
26
|
output,
|
|
27
|
+
project::ProjectSnapshot,
|
|
22
28
|
};
|
|
23
29
|
|
|
24
30
|
#[derive(Debug, Serialize)]
|
|
25
31
|
pub(crate) struct MakeReport {
|
|
26
32
|
package: PackageReport,
|
|
27
33
|
target: String,
|
|
34
|
+
#[serde(skip)]
|
|
35
|
+
target_kind: MakeTarget,
|
|
28
36
|
skip_package: bool,
|
|
29
37
|
dry_run: bool,
|
|
30
38
|
make_dir: Utf8PathBuf,
|
|
@@ -34,27 +42,53 @@ pub(crate) struct MakeReport {
|
|
|
34
42
|
warnings: Vec<String>,
|
|
35
43
|
}
|
|
36
44
|
|
|
37
|
-
#[derive(Debug, Serialize)]
|
|
45
|
+
#[derive(Clone, Copy, Debug, Serialize)]
|
|
38
46
|
#[serde(rename_all = "kebab-case")]
|
|
39
47
|
enum MakeStatus {
|
|
40
48
|
Planned,
|
|
41
49
|
Made,
|
|
42
50
|
}
|
|
43
51
|
|
|
52
|
+
struct ResolvedMakeTargets {
|
|
53
|
+
targets: Vec<MakeTarget>,
|
|
54
|
+
warnings: Vec<String>,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#[derive(Debug, Serialize)]
|
|
58
|
+
struct MakeRunReport<'a> {
|
|
59
|
+
targets: &'a [MakeReport],
|
|
60
|
+
dry_run: bool,
|
|
61
|
+
status: MakeStatus,
|
|
62
|
+
warnings: Vec<String>,
|
|
63
|
+
}
|
|
64
|
+
|
|
44
65
|
pub fn run(args: MakeArgs) -> Result<()> {
|
|
45
|
-
let mut
|
|
66
|
+
let mut reports = build_reports(&args)?;
|
|
46
67
|
|
|
47
68
|
if args.dry_run {
|
|
48
|
-
return
|
|
69
|
+
return print_reports(&reports, args.json, MakeStatus::Planned);
|
|
49
70
|
}
|
|
50
71
|
|
|
51
|
-
|
|
52
|
-
report.mark_made()?;
|
|
72
|
+
execute_make_reports(&mut reports, &args)?;
|
|
53
73
|
|
|
54
|
-
|
|
74
|
+
print_reports(&reports, args.json, MakeStatus::Made)
|
|
55
75
|
}
|
|
56
76
|
|
|
57
77
|
pub(crate) fn build_report(args: &MakeArgs) -> Result<MakeReport> {
|
|
78
|
+
let reports = build_reports(args)?;
|
|
79
|
+
if reports.len() != 1 {
|
|
80
|
+
bail!(
|
|
81
|
+
"Expected one make target, but resolved {}. Pass --target to select one target.",
|
|
82
|
+
reports.len()
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
Ok(reports
|
|
86
|
+
.into_iter()
|
|
87
|
+
.next()
|
|
88
|
+
.expect("length was checked above"))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
pub(crate) fn build_reports(args: &MakeArgs) -> Result<Vec<MakeReport>> {
|
|
58
92
|
let package_args = PackageArgs {
|
|
59
93
|
cwd: args.cwd.clone(),
|
|
60
94
|
out_dir: args.out_dir.clone(),
|
|
@@ -66,28 +100,52 @@ pub(crate) fn build_report(args: &MakeArgs) -> Result<MakeReport> {
|
|
|
66
100
|
json: false,
|
|
67
101
|
};
|
|
68
102
|
let snapshot = crate::project::inspect(&package_args.cwd)?;
|
|
69
|
-
let
|
|
103
|
+
let resolved = resolve_make_targets(&snapshot, args)?;
|
|
104
|
+
let config_warnings = resolved.warnings;
|
|
105
|
+
resolved
|
|
106
|
+
.targets
|
|
107
|
+
.into_iter()
|
|
108
|
+
.map(|target| {
|
|
109
|
+
let package = package::build_report(snapshot.clone(), &package_args)?;
|
|
110
|
+
build_report_for_target(package, target, args, &config_warnings)
|
|
111
|
+
})
|
|
112
|
+
.collect()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fn build_report_for_target(
|
|
116
|
+
package: PackageReport,
|
|
117
|
+
target: MakeTarget,
|
|
118
|
+
args: &MakeArgs,
|
|
119
|
+
config_warnings: &[String],
|
|
120
|
+
) -> Result<MakeReport> {
|
|
70
121
|
let make_dir = Path::new(package.output_dir().as_str())
|
|
71
122
|
.join("make")
|
|
72
|
-
.join(
|
|
123
|
+
.join(target.as_str())
|
|
73
124
|
.join(package.platform())
|
|
74
125
|
.join(package.arch());
|
|
75
|
-
let artifact = make_artifact_path(&make_dir, &package,
|
|
126
|
+
let artifact = make_artifact_path(&make_dir, &package, target);
|
|
76
127
|
|
|
77
128
|
let mut warnings = package.warnings().to_vec();
|
|
78
|
-
|
|
129
|
+
warnings.extend(config_warnings.iter().cloned());
|
|
130
|
+
if matches!(target, MakeTarget::Deb | MakeTarget::Rpm) && package.platform() != "linux" {
|
|
79
131
|
warnings.push(format!(
|
|
80
132
|
"{} maker only supports linux packages; target platform is {}.",
|
|
81
|
-
|
|
133
|
+
target.as_str(),
|
|
82
134
|
package.platform()
|
|
83
135
|
));
|
|
84
136
|
}
|
|
85
|
-
if
|
|
137
|
+
if target == MakeTarget::Dmg && package.platform() != "darwin" {
|
|
86
138
|
warnings.push(format!(
|
|
87
139
|
"dmg maker only supports macOS packages; target platform is {}.",
|
|
88
140
|
package.platform()
|
|
89
141
|
));
|
|
90
142
|
}
|
|
143
|
+
if target == MakeTarget::Msi && package.platform() != "win32" {
|
|
144
|
+
warnings.push(format!(
|
|
145
|
+
"msi maker only supports Windows packages; target platform is {}.",
|
|
146
|
+
package.platform()
|
|
147
|
+
));
|
|
148
|
+
}
|
|
91
149
|
if args.skip_package && !Path::new(package.bundle_dir().as_str()).exists() {
|
|
92
150
|
warnings.push(format!(
|
|
93
151
|
"Package output does not exist: {}.",
|
|
@@ -104,7 +162,8 @@ pub(crate) fn build_report(args: &MakeArgs) -> Result<MakeReport> {
|
|
|
104
162
|
|
|
105
163
|
Ok(MakeReport {
|
|
106
164
|
package,
|
|
107
|
-
target:
|
|
165
|
+
target: target.as_str().to_string(),
|
|
166
|
+
target_kind: target,
|
|
108
167
|
skip_package: args.skip_package,
|
|
109
168
|
dry_run: args.dry_run,
|
|
110
169
|
make_dir: utf8_path(make_dir)?,
|
|
@@ -115,17 +174,215 @@ pub(crate) fn build_report(args: &MakeArgs) -> Result<MakeReport> {
|
|
|
115
174
|
})
|
|
116
175
|
}
|
|
117
176
|
|
|
177
|
+
struct ConfiguredMaker {
|
|
178
|
+
label: String,
|
|
179
|
+
target: Option<MakeTarget>,
|
|
180
|
+
platforms: Vec<String>,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
fn resolve_make_targets(
|
|
184
|
+
snapshot: &ProjectSnapshot,
|
|
185
|
+
args: &MakeArgs,
|
|
186
|
+
) -> Result<ResolvedMakeTargets> {
|
|
187
|
+
if let Some(target) = args.target {
|
|
188
|
+
return Ok(ResolvedMakeTargets {
|
|
189
|
+
targets: vec![target],
|
|
190
|
+
warnings: Vec::new(),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let platform = args.platform.clone().unwrap_or_else(current_platform_label);
|
|
195
|
+
let makers = configured_makers(snapshot)?;
|
|
196
|
+
let mut warnings = Vec::new();
|
|
197
|
+
let mut targets = Vec::new();
|
|
198
|
+
|
|
199
|
+
for maker in &makers {
|
|
200
|
+
let Some(target) = maker.target else {
|
|
201
|
+
warnings.push(format!(
|
|
202
|
+
"Configured maker is not implemented yet and will be skipped: {}.",
|
|
203
|
+
maker.label
|
|
204
|
+
));
|
|
205
|
+
continue;
|
|
206
|
+
};
|
|
207
|
+
if !maker_applies_to_platform(maker, &platform) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if !targets.contains(&target) {
|
|
211
|
+
targets.push(target);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if targets.is_empty() {
|
|
216
|
+
if makers.is_empty() {
|
|
217
|
+
targets.push(MakeTarget::Zip);
|
|
218
|
+
} else {
|
|
219
|
+
warnings.push(format!(
|
|
220
|
+
"No supported configured makers apply to {platform}; defaulting to zip. Pass --target to override."
|
|
221
|
+
));
|
|
222
|
+
targets.push(MakeTarget::Zip);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
Ok(ResolvedMakeTargets { targets, warnings })
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
fn configured_makers(snapshot: &ProjectSnapshot) -> Result<Vec<ConfiguredMaker>> {
|
|
230
|
+
let Some(package_json_path) = &snapshot.package_json else {
|
|
231
|
+
return Ok(Vec::new());
|
|
232
|
+
};
|
|
233
|
+
let package_json_path = Path::new(package_json_path.as_str());
|
|
234
|
+
let raw = fs::read_to_string(package_json_path)
|
|
235
|
+
.with_context(|| format!("Could not read {}", package_json_path.display()))?;
|
|
236
|
+
let package = serde_json::from_str::<JsonValue>(&raw)
|
|
237
|
+
.with_context(|| format!("Could not parse {}", package_json_path.display()))?;
|
|
238
|
+
|
|
239
|
+
let mut makers = Vec::new();
|
|
240
|
+
for value in [
|
|
241
|
+
package
|
|
242
|
+
.get("config")
|
|
243
|
+
.and_then(|config| config.get("forge"))
|
|
244
|
+
.and_then(|forge| forge.get("makers")),
|
|
245
|
+
package
|
|
246
|
+
.get("electronCli")
|
|
247
|
+
.or_else(|| package.get("electron-cli"))
|
|
248
|
+
.and_then(|config| config.get("makers")),
|
|
249
|
+
]
|
|
250
|
+
.into_iter()
|
|
251
|
+
.flatten()
|
|
252
|
+
{
|
|
253
|
+
makers.extend(parse_maker_list(value));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
Ok(makers)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
fn parse_maker_list(value: &JsonValue) -> Vec<ConfiguredMaker> {
|
|
260
|
+
match value {
|
|
261
|
+
JsonValue::Array(values) => values.iter().filter_map(parse_maker).collect(),
|
|
262
|
+
_ => Vec::new(),
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
fn parse_maker(value: &JsonValue) -> Option<ConfiguredMaker> {
|
|
267
|
+
match value {
|
|
268
|
+
JsonValue::String(label) => Some(ConfiguredMaker {
|
|
269
|
+
label: label.clone(),
|
|
270
|
+
target: maker_target(label),
|
|
271
|
+
platforms: Vec::new(),
|
|
272
|
+
}),
|
|
273
|
+
JsonValue::Object(object) => {
|
|
274
|
+
let label = object
|
|
275
|
+
.get("name")
|
|
276
|
+
.or_else(|| object.get("target"))
|
|
277
|
+
.or_else(|| object.get("maker"))
|
|
278
|
+
.and_then(JsonValue::as_str)?
|
|
279
|
+
.to_string();
|
|
280
|
+
Some(ConfiguredMaker {
|
|
281
|
+
target: maker_target(&label),
|
|
282
|
+
platforms: string_values(object.get("platforms")),
|
|
283
|
+
label,
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
_ => None,
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
fn maker_target(label: &str) -> Option<MakeTarget> {
|
|
291
|
+
let label = label.trim().to_ascii_lowercase();
|
|
292
|
+
let compact = label
|
|
293
|
+
.trim_start_matches("@electron-forge/")
|
|
294
|
+
.trim_start_matches("electron-forge-")
|
|
295
|
+
.trim_start_matches("maker-");
|
|
296
|
+
|
|
297
|
+
if matches!(compact, "zip" | "@electron-forge/maker-zip")
|
|
298
|
+
|| label.ends_with("/maker-zip")
|
|
299
|
+
|| label.ends_with("maker-zip")
|
|
300
|
+
{
|
|
301
|
+
Some(MakeTarget::Zip)
|
|
302
|
+
} else if compact == "dmg" || label.ends_with("/maker-dmg") || label.ends_with("maker-dmg") {
|
|
303
|
+
Some(MakeTarget::Dmg)
|
|
304
|
+
} else if compact == "deb" || label.ends_with("/maker-deb") || label.ends_with("maker-deb") {
|
|
305
|
+
Some(MakeTarget::Deb)
|
|
306
|
+
} else if compact == "rpm" || label.ends_with("/maker-rpm") || label.ends_with("maker-rpm") {
|
|
307
|
+
Some(MakeTarget::Rpm)
|
|
308
|
+
} else if matches!(compact, "msi" | "wix")
|
|
309
|
+
|| label.ends_with("/maker-wix")
|
|
310
|
+
|| label.ends_with("maker-wix")
|
|
311
|
+
{
|
|
312
|
+
Some(MakeTarget::Msi)
|
|
313
|
+
} else {
|
|
314
|
+
None
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
fn maker_applies_to_platform(maker: &ConfiguredMaker, platform: &str) -> bool {
|
|
319
|
+
maker.platforms.is_empty()
|
|
320
|
+
|| maker
|
|
321
|
+
.platforms
|
|
322
|
+
.iter()
|
|
323
|
+
.any(|configured| configured == platform || configured == "*")
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
fn string_values(value: Option<&JsonValue>) -> Vec<String> {
|
|
327
|
+
match value {
|
|
328
|
+
Some(JsonValue::String(value)) => vec![value.clone()],
|
|
329
|
+
Some(JsonValue::Array(values)) => values
|
|
330
|
+
.iter()
|
|
331
|
+
.filter_map(JsonValue::as_str)
|
|
332
|
+
.map(ToOwned::to_owned)
|
|
333
|
+
.collect(),
|
|
334
|
+
_ => Vec::new(),
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
fn current_platform_label() -> String {
|
|
339
|
+
if cfg!(target_os = "macos") {
|
|
340
|
+
"darwin".to_string()
|
|
341
|
+
} else if cfg!(target_os = "windows") {
|
|
342
|
+
"win32".to_string()
|
|
343
|
+
} else {
|
|
344
|
+
"linux".to_string()
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
118
348
|
pub(crate) fn execute_make(report: &mut MakeReport, args: &MakeArgs) -> Result<()> {
|
|
349
|
+
ensure_package_ready(std::slice::from_mut(report), args)?;
|
|
350
|
+
execute_make_artifact(report, args)?;
|
|
351
|
+
Ok(())
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
pub(crate) fn execute_make_reports(reports: &mut [MakeReport], args: &MakeArgs) -> Result<()> {
|
|
355
|
+
if reports.is_empty() {
|
|
356
|
+
bail!("No make targets were resolved.");
|
|
357
|
+
}
|
|
358
|
+
ensure_package_ready(reports, args)?;
|
|
359
|
+
for report in reports {
|
|
360
|
+
execute_make_artifact(report, args)?;
|
|
361
|
+
report.mark_made()?;
|
|
362
|
+
}
|
|
363
|
+
Ok(())
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
fn ensure_package_ready(reports: &mut [MakeReport], args: &MakeArgs) -> Result<()> {
|
|
367
|
+
let first = reports
|
|
368
|
+
.first_mut()
|
|
369
|
+
.context("No make targets were resolved.")?;
|
|
119
370
|
if !args.skip_package {
|
|
120
|
-
package::execute_package(&
|
|
121
|
-
report
|
|
122
|
-
|
|
371
|
+
package::execute_package(&first.package, args.force)?;
|
|
372
|
+
for report in reports {
|
|
373
|
+
report.package.mark_packaged();
|
|
374
|
+
}
|
|
375
|
+
} else if !Path::new(first.package.bundle_dir().as_str()).exists() {
|
|
123
376
|
bail!(
|
|
124
377
|
"Package output does not exist: {}. Run without --skip-package or run electron-cli package first.",
|
|
125
|
-
|
|
378
|
+
first.package.bundle_dir()
|
|
126
379
|
);
|
|
127
380
|
}
|
|
128
381
|
|
|
382
|
+
Ok(())
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
fn execute_make_artifact(report: &mut MakeReport, args: &MakeArgs) -> Result<()> {
|
|
129
386
|
let artifact = Path::new(report.artifact.as_str());
|
|
130
387
|
if artifact.exists() {
|
|
131
388
|
if args.force {
|
|
@@ -141,12 +398,13 @@ pub(crate) fn execute_make(report: &mut MakeReport, args: &MakeArgs) -> Result<(
|
|
|
141
398
|
|
|
142
399
|
fs::create_dir_all(report.make_dir.as_str())
|
|
143
400
|
.with_context(|| format!("Could not create {}", report.make_dir))?;
|
|
144
|
-
match
|
|
401
|
+
match report.target_kind {
|
|
145
402
|
MakeTarget::Zip => {
|
|
146
403
|
write_zip_archive(Path::new(report.package.bundle_dir().as_str()), artifact)?
|
|
147
404
|
}
|
|
148
405
|
MakeTarget::Deb => write_deb_archive(&report.package, artifact)?,
|
|
149
406
|
MakeTarget::Dmg => write_dmg_archive(&report.package, artifact)?,
|
|
407
|
+
MakeTarget::Msi => write_msi_archive(&report.package, artifact)?,
|
|
150
408
|
MakeTarget::Rpm => write_rpm_archive(&report.package, artifact)?,
|
|
151
409
|
}
|
|
152
410
|
|
|
@@ -193,6 +451,69 @@ fn print_report(report: &MakeReport, json: bool) -> Result<()> {
|
|
|
193
451
|
Ok(())
|
|
194
452
|
}
|
|
195
453
|
|
|
454
|
+
fn print_reports(reports: &[MakeReport], json: bool, status: MakeStatus) -> Result<()> {
|
|
455
|
+
if reports.len() == 1 {
|
|
456
|
+
return print_report(&reports[0], json);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
let warnings = combined_warnings(reports);
|
|
460
|
+
if json {
|
|
461
|
+
return output::json(&MakeRunReport {
|
|
462
|
+
targets: reports,
|
|
463
|
+
dry_run: reports.iter().any(|report| report.dry_run),
|
|
464
|
+
status,
|
|
465
|
+
warnings,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
println!("electron-cli make");
|
|
470
|
+
println!();
|
|
471
|
+
if let Some(first) = reports.first() {
|
|
472
|
+
println!("Project");
|
|
473
|
+
println!(" root: {}", first.package.project().root);
|
|
474
|
+
match first.package.project().package_label() {
|
|
475
|
+
Some(label) => println!(" package: {label}"),
|
|
476
|
+
None => println!(" package: not found"),
|
|
477
|
+
}
|
|
478
|
+
println!(" app name: {}", first.package.app_name());
|
|
479
|
+
println!(
|
|
480
|
+
" target platform: {} {}",
|
|
481
|
+
first.package.platform(),
|
|
482
|
+
first.package.arch()
|
|
483
|
+
);
|
|
484
|
+
println!(" status: {}", status.as_str());
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
println!();
|
|
488
|
+
println!("Artifacts");
|
|
489
|
+
for report in reports {
|
|
490
|
+
println!(" {}: {}", report.target, report.artifact);
|
|
491
|
+
if let Some(size) = report.artifact_size {
|
|
492
|
+
println!(" size: {size} bytes");
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if !warnings.is_empty() {
|
|
497
|
+
println!();
|
|
498
|
+
println!("Warnings");
|
|
499
|
+
for warning in warnings {
|
|
500
|
+
println!(" {warning}");
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
Ok(())
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
fn combined_warnings(reports: &[MakeReport]) -> Vec<String> {
|
|
508
|
+
let mut warnings = Vec::new();
|
|
509
|
+
for warning in reports.iter().flat_map(|report| report.warnings()) {
|
|
510
|
+
if !warnings.contains(warning) {
|
|
511
|
+
warnings.push(warning.clone());
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
warnings
|
|
515
|
+
}
|
|
516
|
+
|
|
196
517
|
fn make_artifact_path(make_dir: &Path, package: &PackageReport, target: MakeTarget) -> PathBuf {
|
|
197
518
|
match target {
|
|
198
519
|
MakeTarget::Zip => make_dir.join(format!(
|
|
@@ -213,6 +534,12 @@ fn make_artifact_path(make_dir: &Path, package: &PackageReport, target: MakeTarg
|
|
|
213
534
|
dmg_version(package.project().version.as_deref()),
|
|
214
535
|
package.arch()
|
|
215
536
|
)),
|
|
537
|
+
MakeTarget::Msi => make_dir.join(format!(
|
|
538
|
+
"{}-{}-{}.msi",
|
|
539
|
+
package.artifact_stem(),
|
|
540
|
+
windows_artifact_version(package.project().version.as_deref()),
|
|
541
|
+
windows_arch(package.arch())
|
|
542
|
+
)),
|
|
216
543
|
MakeTarget::Rpm => make_dir.join(format!(
|
|
217
544
|
"{}-{}-1.{}.rpm",
|
|
218
545
|
rpm_package_name(&package.artifact_stem()),
|
|
@@ -655,6 +982,627 @@ fn write_rpm_archive(package: &PackageReport, artifact: &Path) -> Result<()> {
|
|
|
655
982
|
.with_context(|| format!("Could not write {}", artifact.display()))
|
|
656
983
|
}
|
|
657
984
|
|
|
985
|
+
#[derive(Debug)]
|
|
986
|
+
struct MsiPayload {
|
|
987
|
+
directories: Vec<MsiDirectoryEntry>,
|
|
988
|
+
files: Vec<MsiFileEntry>,
|
|
989
|
+
shortcut_component: Option<String>,
|
|
990
|
+
shortcut_target_file: Option<String>,
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
#[derive(Debug)]
|
|
994
|
+
struct MsiDirectoryEntry {
|
|
995
|
+
id: String,
|
|
996
|
+
parent_id: String,
|
|
997
|
+
name: String,
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
#[derive(Debug)]
|
|
1001
|
+
struct MsiFileEntry {
|
|
1002
|
+
id: String,
|
|
1003
|
+
component_id: String,
|
|
1004
|
+
component_guid: String,
|
|
1005
|
+
directory_id: String,
|
|
1006
|
+
source: PathBuf,
|
|
1007
|
+
file_name: String,
|
|
1008
|
+
cabinet_name: String,
|
|
1009
|
+
size: i32,
|
|
1010
|
+
sequence: i32,
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
fn write_msi_archive(package: &PackageReport, artifact: &Path) -> Result<()> {
|
|
1014
|
+
if package.platform() != "win32" {
|
|
1015
|
+
bail!(
|
|
1016
|
+
"MSI maker only supports Windows packages. Requested {}.",
|
|
1017
|
+
package.platform()
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
let source = Path::new(package.bundle_dir().as_str());
|
|
1022
|
+
if !source.exists() {
|
|
1023
|
+
bail!("Package output does not exist: {}", source.display());
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
let parent = artifact
|
|
1027
|
+
.parent()
|
|
1028
|
+
.with_context(|| format!("Artifact path has no parent: {}", artifact.display()))?;
|
|
1029
|
+
fs::create_dir_all(parent).with_context(|| format!("Could not create {}", parent.display()))?;
|
|
1030
|
+
|
|
1031
|
+
let payload = collect_msi_payload(package, source)?;
|
|
1032
|
+
if payload.files.is_empty() {
|
|
1033
|
+
bail!(
|
|
1034
|
+
"MSI maker requires at least one packaged file in {}",
|
|
1035
|
+
source.display()
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
let cabinet = create_msi_cabinet(&payload)?;
|
|
1040
|
+
let file = fs::OpenOptions::new()
|
|
1041
|
+
.read(true)
|
|
1042
|
+
.write(true)
|
|
1043
|
+
.create(true)
|
|
1044
|
+
.truncate(true)
|
|
1045
|
+
.open(artifact)
|
|
1046
|
+
.with_context(|| format!("Could not create {}", artifact.display()))?;
|
|
1047
|
+
let mut installer =
|
|
1048
|
+
Package::create(PackageType::Installer, file).context("Could not create MSI package")?;
|
|
1049
|
+
|
|
1050
|
+
write_msi_summary(&mut installer, package)?;
|
|
1051
|
+
create_msi_tables(&mut installer)?;
|
|
1052
|
+
insert_msi_rows(&mut installer, package, &payload)?;
|
|
1053
|
+
{
|
|
1054
|
+
let mut stream = installer
|
|
1055
|
+
.write_stream("app.cab")
|
|
1056
|
+
.context("Could not create embedded MSI cabinet stream")?;
|
|
1057
|
+
stream
|
|
1058
|
+
.write_all(&cabinet)
|
|
1059
|
+
.context("Could not write embedded MSI cabinet stream")?;
|
|
1060
|
+
}
|
|
1061
|
+
installer.flush().context("Could not flush MSI package")?;
|
|
1062
|
+
installer
|
|
1063
|
+
.into_inner()
|
|
1064
|
+
.context("Could not finish MSI package")?;
|
|
1065
|
+
|
|
1066
|
+
Ok(())
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
fn write_msi_summary(installer: &mut Package<File>, package: &PackageReport) -> Result<()> {
|
|
1070
|
+
let package_code = deterministic_guid(
|
|
1071
|
+
"package-code",
|
|
1072
|
+
&[
|
|
1073
|
+
package.app_name(),
|
|
1074
|
+
package.project().version.as_deref().unwrap_or("0.1.0"),
|
|
1075
|
+
package.arch(),
|
|
1076
|
+
],
|
|
1077
|
+
);
|
|
1078
|
+
let arch = msi_summary_arch(package.arch());
|
|
1079
|
+
let language = Language::from_code(1033);
|
|
1080
|
+
let summary = installer.summary_info_mut();
|
|
1081
|
+
summary.set_title(format!("{} Installer", package.app_name()));
|
|
1082
|
+
summary.set_subject(package.app_name().to_string());
|
|
1083
|
+
summary.set_author("electron-cli".to_string());
|
|
1084
|
+
summary.set_comments(format!(
|
|
1085
|
+
"{} packaged by electron-cli.",
|
|
1086
|
+
single_line(package.app_name())
|
|
1087
|
+
));
|
|
1088
|
+
summary.set_creating_application("electron-cli".to_string());
|
|
1089
|
+
summary.set_uuid(package_code);
|
|
1090
|
+
summary.set_arch(arch.to_string());
|
|
1091
|
+
summary.set_languages(&[language]);
|
|
1092
|
+
summary.set_page_count(200);
|
|
1093
|
+
summary.set_word_count(2);
|
|
1094
|
+
Ok(())
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
fn create_msi_tables(installer: &mut Package<File>) -> Result<()> {
|
|
1098
|
+
create_msi_table(
|
|
1099
|
+
installer,
|
|
1100
|
+
"Property",
|
|
1101
|
+
vec![
|
|
1102
|
+
Column::build("Property").primary_key().id_string(72),
|
|
1103
|
+
Column::build("Value").nullable().formatted_string(0),
|
|
1104
|
+
],
|
|
1105
|
+
)?;
|
|
1106
|
+
create_msi_table(
|
|
1107
|
+
installer,
|
|
1108
|
+
"Directory",
|
|
1109
|
+
vec![
|
|
1110
|
+
Column::build("Directory").primary_key().id_string(72),
|
|
1111
|
+
Column::build("Directory_Parent").nullable().id_string(72),
|
|
1112
|
+
Column::build("DefaultDir").text_string(255),
|
|
1113
|
+
],
|
|
1114
|
+
)?;
|
|
1115
|
+
create_msi_table(
|
|
1116
|
+
installer,
|
|
1117
|
+
"Feature",
|
|
1118
|
+
vec![
|
|
1119
|
+
Column::build("Feature").primary_key().id_string(38),
|
|
1120
|
+
Column::build("Feature_Parent").nullable().id_string(38),
|
|
1121
|
+
Column::build("Title").nullable().text_string(64),
|
|
1122
|
+
Column::build("Description").nullable().text_string(255),
|
|
1123
|
+
Column::build("Display").nullable().int16(),
|
|
1124
|
+
Column::build("Level").int16(),
|
|
1125
|
+
Column::build("Directory_").nullable().id_string(72),
|
|
1126
|
+
Column::build("Attributes").int16(),
|
|
1127
|
+
],
|
|
1128
|
+
)?;
|
|
1129
|
+
create_msi_table(
|
|
1130
|
+
installer,
|
|
1131
|
+
"Component",
|
|
1132
|
+
vec![
|
|
1133
|
+
Column::build("Component").primary_key().id_string(72),
|
|
1134
|
+
Column::build("ComponentId").nullable().string(38),
|
|
1135
|
+
Column::build("Directory_").id_string(72),
|
|
1136
|
+
Column::build("Attributes").int16(),
|
|
1137
|
+
Column::build("Condition").nullable().formatted_string(255),
|
|
1138
|
+
Column::build("KeyPath").nullable().id_string(72),
|
|
1139
|
+
],
|
|
1140
|
+
)?;
|
|
1141
|
+
create_msi_table(
|
|
1142
|
+
installer,
|
|
1143
|
+
"FeatureComponents",
|
|
1144
|
+
vec![
|
|
1145
|
+
Column::build("Feature_").primary_key().id_string(38),
|
|
1146
|
+
Column::build("Component_").primary_key().id_string(72),
|
|
1147
|
+
],
|
|
1148
|
+
)?;
|
|
1149
|
+
create_msi_table(
|
|
1150
|
+
installer,
|
|
1151
|
+
"File",
|
|
1152
|
+
vec![
|
|
1153
|
+
Column::build("File").primary_key().id_string(72),
|
|
1154
|
+
Column::build("Component_").id_string(72),
|
|
1155
|
+
Column::build("FileName").text_string(255),
|
|
1156
|
+
Column::build("FileSize").int32(),
|
|
1157
|
+
Column::build("Version").nullable().string(72),
|
|
1158
|
+
Column::build("Language").nullable().string(20),
|
|
1159
|
+
Column::build("Attributes").nullable().int16(),
|
|
1160
|
+
Column::build("Sequence").int16(),
|
|
1161
|
+
],
|
|
1162
|
+
)?;
|
|
1163
|
+
create_msi_table(
|
|
1164
|
+
installer,
|
|
1165
|
+
"Media",
|
|
1166
|
+
vec![
|
|
1167
|
+
Column::build("DiskId").primary_key().int16(),
|
|
1168
|
+
Column::build("LastSequence").int16(),
|
|
1169
|
+
Column::build("DiskPrompt").nullable().text_string(64),
|
|
1170
|
+
Column::build("Cabinet").nullable().string(255),
|
|
1171
|
+
Column::build("VolumeLabel").nullable().string(32),
|
|
1172
|
+
Column::build("Source").nullable().string(72),
|
|
1173
|
+
],
|
|
1174
|
+
)?;
|
|
1175
|
+
create_msi_table(
|
|
1176
|
+
installer,
|
|
1177
|
+
"Shortcut",
|
|
1178
|
+
vec![
|
|
1179
|
+
Column::build("Shortcut").primary_key().id_string(72),
|
|
1180
|
+
Column::build("Directory_").id_string(72),
|
|
1181
|
+
Column::build("Name").text_string(128),
|
|
1182
|
+
Column::build("Component_").id_string(72),
|
|
1183
|
+
Column::build("Target").formatted_string(0),
|
|
1184
|
+
Column::build("Arguments").nullable().formatted_string(255),
|
|
1185
|
+
Column::build("Description").nullable().text_string(255),
|
|
1186
|
+
Column::build("Hotkey").nullable().int16(),
|
|
1187
|
+
Column::build("Icon_").nullable().id_string(72),
|
|
1188
|
+
Column::build("IconIndex").nullable().int16(),
|
|
1189
|
+
Column::build("ShowCmd").nullable().int16(),
|
|
1190
|
+
Column::build("WkDir").nullable().id_string(72),
|
|
1191
|
+
],
|
|
1192
|
+
)?;
|
|
1193
|
+
create_msi_table(
|
|
1194
|
+
installer,
|
|
1195
|
+
"RemoveFile",
|
|
1196
|
+
vec![
|
|
1197
|
+
Column::build("FileKey").primary_key().id_string(72),
|
|
1198
|
+
Column::build("Component_").id_string(72),
|
|
1199
|
+
Column::build("FileName").nullable().text_string(255),
|
|
1200
|
+
Column::build("DirProperty").id_string(72),
|
|
1201
|
+
Column::build("InstallMode").int16(),
|
|
1202
|
+
],
|
|
1203
|
+
)?;
|
|
1204
|
+
create_msi_table(
|
|
1205
|
+
installer,
|
|
1206
|
+
"InstallExecuteSequence",
|
|
1207
|
+
vec![
|
|
1208
|
+
Column::build("Action").primary_key().id_string(72),
|
|
1209
|
+
Column::build("Condition").nullable().formatted_string(255),
|
|
1210
|
+
Column::build("Sequence").nullable().int16(),
|
|
1211
|
+
],
|
|
1212
|
+
)?;
|
|
1213
|
+
create_msi_table(
|
|
1214
|
+
installer,
|
|
1215
|
+
"ActionText",
|
|
1216
|
+
vec![
|
|
1217
|
+
Column::build("Action").primary_key().id_string(72),
|
|
1218
|
+
Column::build("Description").nullable().text_string(64),
|
|
1219
|
+
Column::build("Template").nullable().formatted_string(128),
|
|
1220
|
+
],
|
|
1221
|
+
)
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
fn create_msi_table(installer: &mut Package<File>, name: &str, columns: Vec<Column>) -> Result<()> {
|
|
1225
|
+
installer
|
|
1226
|
+
.create_table(name, columns)
|
|
1227
|
+
.with_context(|| format!("Could not create MSI {name} table"))
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
fn insert_msi_rows(
|
|
1231
|
+
installer: &mut Package<File>,
|
|
1232
|
+
package: &PackageReport,
|
|
1233
|
+
payload: &MsiPayload,
|
|
1234
|
+
) -> Result<()> {
|
|
1235
|
+
let product_version = msi_product_version(package.project().version.as_deref());
|
|
1236
|
+
let product_code = msi_guid(deterministic_guid(
|
|
1237
|
+
"product-code",
|
|
1238
|
+
&[package.app_name(), &product_version, package.arch()],
|
|
1239
|
+
));
|
|
1240
|
+
let upgrade_code = msi_guid(deterministic_guid(
|
|
1241
|
+
"upgrade-code",
|
|
1242
|
+
&[
|
|
1243
|
+
package.app_name(),
|
|
1244
|
+
package.project().name.as_deref().unwrap_or(""),
|
|
1245
|
+
],
|
|
1246
|
+
));
|
|
1247
|
+
insert_msi_table_rows(
|
|
1248
|
+
installer,
|
|
1249
|
+
"Property",
|
|
1250
|
+
vec![
|
|
1251
|
+
vec![s("ProductCode"), s(product_code)],
|
|
1252
|
+
vec![s("ProductLanguage"), s("1033")],
|
|
1253
|
+
vec![s("ProductName"), s(package.app_name())],
|
|
1254
|
+
vec![s("ProductVersion"), s(product_version)],
|
|
1255
|
+
vec![s("Manufacturer"), s("electron-cli")],
|
|
1256
|
+
vec![s("UpgradeCode"), s(upgrade_code)],
|
|
1257
|
+
vec![s("ALLUSERS"), s("1")],
|
|
1258
|
+
vec![s("INSTALLLEVEL"), s("1")],
|
|
1259
|
+
],
|
|
1260
|
+
)?;
|
|
1261
|
+
|
|
1262
|
+
let program_files_dir = msi_program_files_directory(package.arch());
|
|
1263
|
+
let install_folder = msi_filename("APPDIR", package.app_name());
|
|
1264
|
+
insert_msi_table_rows(
|
|
1265
|
+
installer,
|
|
1266
|
+
"Directory",
|
|
1267
|
+
vec![
|
|
1268
|
+
vec![s("TARGETDIR"), Value::Null, s("SourceDir")],
|
|
1269
|
+
vec![s(program_files_dir), s("TARGETDIR"), s(".")],
|
|
1270
|
+
vec![s("INSTALLFOLDER"), s(program_files_dir), s(install_folder)],
|
|
1271
|
+
vec![s("ProgramMenuFolder"), s("TARGETDIR"), s(".")],
|
|
1272
|
+
vec![
|
|
1273
|
+
s("ApplicationProgramsFolder"),
|
|
1274
|
+
s("ProgramMenuFolder"),
|
|
1275
|
+
s(msi_filename("APPMENU", package.app_name())),
|
|
1276
|
+
],
|
|
1277
|
+
],
|
|
1278
|
+
)?;
|
|
1279
|
+
insert_msi_table_rows(
|
|
1280
|
+
installer,
|
|
1281
|
+
"Directory",
|
|
1282
|
+
payload
|
|
1283
|
+
.directories
|
|
1284
|
+
.iter()
|
|
1285
|
+
.map(|directory| {
|
|
1286
|
+
vec![
|
|
1287
|
+
s(&directory.id),
|
|
1288
|
+
s(&directory.parent_id),
|
|
1289
|
+
s(&directory.name),
|
|
1290
|
+
]
|
|
1291
|
+
})
|
|
1292
|
+
.collect(),
|
|
1293
|
+
)?;
|
|
1294
|
+
|
|
1295
|
+
insert_msi_table_rows(
|
|
1296
|
+
installer,
|
|
1297
|
+
"Feature",
|
|
1298
|
+
vec![vec![
|
|
1299
|
+
s("MainFeature"),
|
|
1300
|
+
Value::Null,
|
|
1301
|
+
s(package.app_name()),
|
|
1302
|
+
s(format!("Install {}.", single_line(package.app_name()))),
|
|
1303
|
+
Value::from(1),
|
|
1304
|
+
Value::from(1),
|
|
1305
|
+
s("INSTALLFOLDER"),
|
|
1306
|
+
Value::from(0),
|
|
1307
|
+
]],
|
|
1308
|
+
)?;
|
|
1309
|
+
|
|
1310
|
+
let component_attributes = msi_component_attributes(package.arch());
|
|
1311
|
+
insert_msi_table_rows(
|
|
1312
|
+
installer,
|
|
1313
|
+
"Component",
|
|
1314
|
+
payload
|
|
1315
|
+
.files
|
|
1316
|
+
.iter()
|
|
1317
|
+
.map(|file| {
|
|
1318
|
+
vec![
|
|
1319
|
+
s(&file.component_id),
|
|
1320
|
+
s(&file.component_guid),
|
|
1321
|
+
s(&file.directory_id),
|
|
1322
|
+
Value::from(component_attributes),
|
|
1323
|
+
Value::Null,
|
|
1324
|
+
s(&file.id),
|
|
1325
|
+
]
|
|
1326
|
+
})
|
|
1327
|
+
.collect(),
|
|
1328
|
+
)?;
|
|
1329
|
+
insert_msi_table_rows(
|
|
1330
|
+
installer,
|
|
1331
|
+
"FeatureComponents",
|
|
1332
|
+
payload
|
|
1333
|
+
.files
|
|
1334
|
+
.iter()
|
|
1335
|
+
.map(|file| vec![s("MainFeature"), s(&file.component_id)])
|
|
1336
|
+
.collect(),
|
|
1337
|
+
)?;
|
|
1338
|
+
insert_msi_table_rows(
|
|
1339
|
+
installer,
|
|
1340
|
+
"File",
|
|
1341
|
+
payload
|
|
1342
|
+
.files
|
|
1343
|
+
.iter()
|
|
1344
|
+
.map(|file| {
|
|
1345
|
+
vec![
|
|
1346
|
+
s(&file.id),
|
|
1347
|
+
s(&file.component_id),
|
|
1348
|
+
s(&file.file_name),
|
|
1349
|
+
Value::from(file.size),
|
|
1350
|
+
Value::Null,
|
|
1351
|
+
Value::Null,
|
|
1352
|
+
Value::from(0),
|
|
1353
|
+
Value::from(file.sequence),
|
|
1354
|
+
]
|
|
1355
|
+
})
|
|
1356
|
+
.collect(),
|
|
1357
|
+
)?;
|
|
1358
|
+
insert_msi_table_rows(
|
|
1359
|
+
installer,
|
|
1360
|
+
"Media",
|
|
1361
|
+
vec![vec![
|
|
1362
|
+
Value::from(1),
|
|
1363
|
+
Value::from(payload.files.len() as i32),
|
|
1364
|
+
Value::Null,
|
|
1365
|
+
s("#app.cab"),
|
|
1366
|
+
Value::Null,
|
|
1367
|
+
Value::Null,
|
|
1368
|
+
]],
|
|
1369
|
+
)?;
|
|
1370
|
+
|
|
1371
|
+
if let (Some(component), Some(target_file)) =
|
|
1372
|
+
(&payload.shortcut_component, &payload.shortcut_target_file)
|
|
1373
|
+
{
|
|
1374
|
+
insert_msi_table_rows(
|
|
1375
|
+
installer,
|
|
1376
|
+
"Shortcut",
|
|
1377
|
+
vec![vec![
|
|
1378
|
+
s("ApplicationShortcut"),
|
|
1379
|
+
s("ApplicationProgramsFolder"),
|
|
1380
|
+
s(msi_filename("SHORTCUT", package.app_name())),
|
|
1381
|
+
s(component),
|
|
1382
|
+
s(format!("[#{target_file}]")),
|
|
1383
|
+
Value::Null,
|
|
1384
|
+
s(format!("Launch {}.", single_line(package.app_name()))),
|
|
1385
|
+
Value::Null,
|
|
1386
|
+
Value::Null,
|
|
1387
|
+
Value::Null,
|
|
1388
|
+
Value::Null,
|
|
1389
|
+
s("INSTALLFOLDER"),
|
|
1390
|
+
]],
|
|
1391
|
+
)?;
|
|
1392
|
+
insert_msi_table_rows(
|
|
1393
|
+
installer,
|
|
1394
|
+
"RemoveFile",
|
|
1395
|
+
vec![vec![
|
|
1396
|
+
s("RemoveStartMenuFolder"),
|
|
1397
|
+
s(component),
|
|
1398
|
+
Value::Null,
|
|
1399
|
+
s("ApplicationProgramsFolder"),
|
|
1400
|
+
Value::from(2),
|
|
1401
|
+
]],
|
|
1402
|
+
)?;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
insert_msi_table_rows(
|
|
1406
|
+
installer,
|
|
1407
|
+
"InstallExecuteSequence",
|
|
1408
|
+
vec![
|
|
1409
|
+
standard_action("CostInitialize", 800),
|
|
1410
|
+
standard_action("FileCost", 900),
|
|
1411
|
+
standard_action("CostFinalize", 1000),
|
|
1412
|
+
standard_action("InstallValidate", 1400),
|
|
1413
|
+
standard_action("InstallInitialize", 1500),
|
|
1414
|
+
standard_action("ProcessComponents", 1600),
|
|
1415
|
+
standard_action("UnpublishFeatures", 1800),
|
|
1416
|
+
standard_action("RemoveShortcuts", 3200),
|
|
1417
|
+
standard_action("RemoveFiles", 3500),
|
|
1418
|
+
standard_action("InstallFiles", 4000),
|
|
1419
|
+
standard_action("CreateShortcuts", 4500),
|
|
1420
|
+
standard_action("RegisterUser", 6000),
|
|
1421
|
+
standard_action("RegisterProduct", 6100),
|
|
1422
|
+
standard_action("PublishFeatures", 6300),
|
|
1423
|
+
standard_action("PublishProduct", 6400),
|
|
1424
|
+
standard_action("InstallFinalize", 6600),
|
|
1425
|
+
],
|
|
1426
|
+
)?;
|
|
1427
|
+
insert_msi_table_rows(
|
|
1428
|
+
installer,
|
|
1429
|
+
"ActionText",
|
|
1430
|
+
vec![
|
|
1431
|
+
action_text(
|
|
1432
|
+
"InstallFiles",
|
|
1433
|
+
"Copying new files",
|
|
1434
|
+
"File: [1], Directory: [9], Size: [6]",
|
|
1435
|
+
),
|
|
1436
|
+
action_text("CreateShortcuts", "Creating shortcuts", "Shortcut: [1]"),
|
|
1437
|
+
action_text("RemoveFiles", "Removing files", "File: [1], Directory: [9]"),
|
|
1438
|
+
action_text("RemoveShortcuts", "Removing shortcuts", "Shortcut: [1]"),
|
|
1439
|
+
],
|
|
1440
|
+
)
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
fn insert_msi_table_rows(
|
|
1444
|
+
installer: &mut Package<File>,
|
|
1445
|
+
table: &str,
|
|
1446
|
+
rows: Vec<Vec<Value>>,
|
|
1447
|
+
) -> Result<()> {
|
|
1448
|
+
if rows.is_empty() {
|
|
1449
|
+
return Ok(());
|
|
1450
|
+
}
|
|
1451
|
+
installer
|
|
1452
|
+
.insert_rows(Insert::into(table).rows(rows))
|
|
1453
|
+
.with_context(|| format!("Could not insert MSI {table} rows"))
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
fn standard_action(action: &str, sequence: i32) -> Vec<Value> {
|
|
1457
|
+
vec![s(action), Value::Null, Value::from(sequence)]
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
fn action_text(action: &str, description: &str, template: &str) -> Vec<Value> {
|
|
1461
|
+
vec![s(action), s(description), s(template)]
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
fn collect_msi_payload(package: &PackageReport, source: &Path) -> Result<MsiPayload> {
|
|
1465
|
+
let mut payload = MsiPayload {
|
|
1466
|
+
directories: Vec::new(),
|
|
1467
|
+
files: Vec::new(),
|
|
1468
|
+
shortcut_component: None,
|
|
1469
|
+
shortcut_target_file: None,
|
|
1470
|
+
};
|
|
1471
|
+
let mut directory_ids = BTreeMap::from([(PathBuf::new(), "INSTALLFOLDER".to_string())]);
|
|
1472
|
+
collect_msi_directory(
|
|
1473
|
+
package,
|
|
1474
|
+
source,
|
|
1475
|
+
Path::new(""),
|
|
1476
|
+
"INSTALLFOLDER",
|
|
1477
|
+
&mut directory_ids,
|
|
1478
|
+
&mut payload,
|
|
1479
|
+
)?;
|
|
1480
|
+
|
|
1481
|
+
if payload.files.len() > i16::MAX as usize {
|
|
1482
|
+
bail!(
|
|
1483
|
+
"MSI maker supports up to {} files; package contains {}.",
|
|
1484
|
+
i16::MAX,
|
|
1485
|
+
payload.files.len()
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
Ok(payload)
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
fn collect_msi_directory(
|
|
1493
|
+
package: &PackageReport,
|
|
1494
|
+
source: &Path,
|
|
1495
|
+
relative_dir: &Path,
|
|
1496
|
+
directory_id: &str,
|
|
1497
|
+
directory_ids: &mut BTreeMap<PathBuf, String>,
|
|
1498
|
+
payload: &mut MsiPayload,
|
|
1499
|
+
) -> Result<()> {
|
|
1500
|
+
let mut entries = fs::read_dir(source)
|
|
1501
|
+
.with_context(|| format!("Could not read {}", source.display()))?
|
|
1502
|
+
.collect::<Result<Vec<_>, io::Error>>()?;
|
|
1503
|
+
entries.sort_by_key(|entry| entry.path());
|
|
1504
|
+
|
|
1505
|
+
for entry in entries {
|
|
1506
|
+
let path = entry.path();
|
|
1507
|
+
let file_name = utf8_file_name(&path)?.to_string();
|
|
1508
|
+
let relative_path = relative_dir.join(&file_name);
|
|
1509
|
+
let metadata = fs::symlink_metadata(&path)
|
|
1510
|
+
.with_context(|| format!("Could not stat {}", path.display()))?;
|
|
1511
|
+
|
|
1512
|
+
if metadata.file_type().is_symlink() {
|
|
1513
|
+
bail!(
|
|
1514
|
+
"MSI maker does not support symbolic links yet: {}",
|
|
1515
|
+
path.display()
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
if metadata.is_dir() {
|
|
1520
|
+
let dir_id = format!("D{:04}", directory_ids.len());
|
|
1521
|
+
directory_ids.insert(relative_path.clone(), dir_id.clone());
|
|
1522
|
+
payload.directories.push(MsiDirectoryEntry {
|
|
1523
|
+
id: dir_id.clone(),
|
|
1524
|
+
parent_id: directory_id.to_string(),
|
|
1525
|
+
name: msi_filename(&dir_id, &file_name),
|
|
1526
|
+
});
|
|
1527
|
+
collect_msi_directory(
|
|
1528
|
+
package,
|
|
1529
|
+
&path,
|
|
1530
|
+
&relative_path,
|
|
1531
|
+
&dir_id,
|
|
1532
|
+
directory_ids,
|
|
1533
|
+
payload,
|
|
1534
|
+
)?;
|
|
1535
|
+
} else if metadata.is_file() {
|
|
1536
|
+
let sequence = payload.files.len() + 1;
|
|
1537
|
+
let size = i32::try_from(metadata.len())
|
|
1538
|
+
.with_context(|| format!("MSI file is too large: {}", path.display()))?;
|
|
1539
|
+
let file_id = format!("F{sequence:04}");
|
|
1540
|
+
let component_id = format!("C{sequence:04}");
|
|
1541
|
+
let relative_key = relative_path.to_string_lossy().replace('\\', "/");
|
|
1542
|
+
let component_guid = msi_guid(deterministic_guid(
|
|
1543
|
+
"component",
|
|
1544
|
+
&[
|
|
1545
|
+
package.app_name(),
|
|
1546
|
+
package.project().name.as_deref().unwrap_or(""),
|
|
1547
|
+
&relative_key,
|
|
1548
|
+
],
|
|
1549
|
+
));
|
|
1550
|
+
let entry = MsiFileEntry {
|
|
1551
|
+
id: file_id.clone(),
|
|
1552
|
+
component_id: component_id.clone(),
|
|
1553
|
+
component_guid,
|
|
1554
|
+
directory_id: directory_id.to_string(),
|
|
1555
|
+
source: path.clone(),
|
|
1556
|
+
file_name: msi_filename(&file_id, &file_name),
|
|
1557
|
+
cabinet_name: file_id.clone(),
|
|
1558
|
+
size,
|
|
1559
|
+
sequence: sequence as i32,
|
|
1560
|
+
};
|
|
1561
|
+
|
|
1562
|
+
if file_name.eq_ignore_ascii_case(package.executable_name()) {
|
|
1563
|
+
payload.shortcut_component = Some(component_id);
|
|
1564
|
+
payload.shortcut_target_file = Some(file_id);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
payload.files.push(entry);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
Ok(())
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
fn create_msi_cabinet(payload: &MsiPayload) -> Result<Vec<u8>> {
|
|
1575
|
+
let mut builder = CabinetBuilder::new();
|
|
1576
|
+
{
|
|
1577
|
+
let folder = builder.add_folder(CabCompressionType::MsZip);
|
|
1578
|
+
for file in &payload.files {
|
|
1579
|
+
folder.add_file(&file.cabinet_name);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
let cursor = Cursor::new(Vec::new());
|
|
1584
|
+
let mut cabinet = builder
|
|
1585
|
+
.build(cursor)
|
|
1586
|
+
.context("Could not start MSI cabinet")?;
|
|
1587
|
+
for file in &payload.files {
|
|
1588
|
+
let mut writer = cabinet
|
|
1589
|
+
.next_file()
|
|
1590
|
+
.context("Could not open next MSI cabinet file")?
|
|
1591
|
+
.context("MSI cabinet writer finished before all files were written")?;
|
|
1592
|
+
anyhow::ensure!(
|
|
1593
|
+
writer.file_name() == file.cabinet_name,
|
|
1594
|
+
"MSI cabinet file order drifted while writing {}",
|
|
1595
|
+
file.source.display()
|
|
1596
|
+
);
|
|
1597
|
+
let mut source = File::open(&file.source)
|
|
1598
|
+
.with_context(|| format!("Could not open {}", file.source.display()))?;
|
|
1599
|
+
io::copy(&mut source, &mut writer)
|
|
1600
|
+
.with_context(|| format!("Could not add {} to MSI cabinet", file.source.display()))?;
|
|
1601
|
+
}
|
|
1602
|
+
let cursor = cabinet.finish().context("Could not finish MSI cabinet")?;
|
|
1603
|
+
Ok(cursor.into_inner())
|
|
1604
|
+
}
|
|
1605
|
+
|
|
658
1606
|
fn debian_control_file(
|
|
659
1607
|
package: &PackageReport,
|
|
660
1608
|
deb_package: &str,
|
|
@@ -1024,6 +1972,51 @@ fn rpm_version(version: Option<&str>) -> String {
|
|
|
1024
1972
|
}
|
|
1025
1973
|
}
|
|
1026
1974
|
|
|
1975
|
+
fn windows_artifact_version(version: Option<&str>) -> String {
|
|
1976
|
+
let version = version.unwrap_or("0.1.0");
|
|
1977
|
+
let sanitized = version
|
|
1978
|
+
.chars()
|
|
1979
|
+
.map(|char| {
|
|
1980
|
+
if char.is_ascii_alphanumeric() || matches!(char, '.' | '-' | '_') {
|
|
1981
|
+
char
|
|
1982
|
+
} else {
|
|
1983
|
+
'-'
|
|
1984
|
+
}
|
|
1985
|
+
})
|
|
1986
|
+
.collect::<String>()
|
|
1987
|
+
.trim_matches(['-', '.', '_'])
|
|
1988
|
+
.to_string();
|
|
1989
|
+
|
|
1990
|
+
if sanitized.is_empty() {
|
|
1991
|
+
"0.1.0".to_string()
|
|
1992
|
+
} else {
|
|
1993
|
+
sanitized
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
fn msi_product_version(version: Option<&str>) -> String {
|
|
1998
|
+
let mut numbers = version
|
|
1999
|
+
.unwrap_or("0.1.0")
|
|
2000
|
+
.split(|char: char| !char.is_ascii_digit())
|
|
2001
|
+
.filter(|part| !part.is_empty())
|
|
2002
|
+
.filter_map(|part| part.parse::<u32>().ok())
|
|
2003
|
+
.take(3)
|
|
2004
|
+
.collect::<Vec<_>>();
|
|
2005
|
+
while numbers.len() < 3 {
|
|
2006
|
+
numbers.push(0);
|
|
2007
|
+
}
|
|
2008
|
+
if numbers.iter().all(|number| *number == 0) {
|
|
2009
|
+
numbers = vec![0, 1, 0];
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
format!(
|
|
2013
|
+
"{}.{}.{}",
|
|
2014
|
+
numbers[0].min(255),
|
|
2015
|
+
numbers[1].min(255),
|
|
2016
|
+
numbers[2].min(65_535)
|
|
2017
|
+
)
|
|
2018
|
+
}
|
|
2019
|
+
|
|
1027
2020
|
fn debian_arch(arch: &str) -> String {
|
|
1028
2021
|
match arch {
|
|
1029
2022
|
"x64" => "amd64".to_string(),
|
|
@@ -1043,6 +2036,99 @@ fn rpm_arch(arch: &str) -> String {
|
|
|
1043
2036
|
}
|
|
1044
2037
|
}
|
|
1045
2038
|
|
|
2039
|
+
fn windows_arch(arch: &str) -> String {
|
|
2040
|
+
match arch {
|
|
2041
|
+
"ia32" => "x86".to_string(),
|
|
2042
|
+
arch => arch.to_string(),
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
fn msi_summary_arch(arch: &str) -> &'static str {
|
|
2047
|
+
match arch {
|
|
2048
|
+
"x64" => "x64",
|
|
2049
|
+
"arm64" => "Arm64",
|
|
2050
|
+
_ => "Intel",
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
fn msi_program_files_directory(arch: &str) -> &'static str {
|
|
2055
|
+
match arch {
|
|
2056
|
+
"x64" | "arm64" => "ProgramFiles64Folder",
|
|
2057
|
+
_ => "ProgramFilesFolder",
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
fn msi_component_attributes(arch: &str) -> i32 {
|
|
2062
|
+
match arch {
|
|
2063
|
+
"x64" | "arm64" => 256,
|
|
2064
|
+
_ => 0,
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
fn msi_filename(id: &str, long_name: &str) -> String {
|
|
2069
|
+
if is_msi_short_name(long_name) {
|
|
2070
|
+
return long_name.to_string();
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
let extension = Path::new(long_name)
|
|
2074
|
+
.extension()
|
|
2075
|
+
.and_then(|extension| extension.to_str())
|
|
2076
|
+
.map(|extension| {
|
|
2077
|
+
extension
|
|
2078
|
+
.chars()
|
|
2079
|
+
.filter(|char| char.is_ascii_alphanumeric())
|
|
2080
|
+
.take(3)
|
|
2081
|
+
.collect::<String>()
|
|
2082
|
+
})
|
|
2083
|
+
.filter(|extension| !extension.is_empty());
|
|
2084
|
+
let stem = id
|
|
2085
|
+
.chars()
|
|
2086
|
+
.filter(|char| char.is_ascii_alphanumeric())
|
|
2087
|
+
.take(8)
|
|
2088
|
+
.collect::<String>();
|
|
2089
|
+
let short = match extension {
|
|
2090
|
+
Some(extension) => format!("{stem}.{extension}"),
|
|
2091
|
+
None => stem,
|
|
2092
|
+
};
|
|
2093
|
+
format!("{short}|{long_name}")
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
fn is_msi_short_name(name: &str) -> bool {
|
|
2097
|
+
let Some(file_name) = Path::new(name).file_name().and_then(|name| name.to_str()) else {
|
|
2098
|
+
return false;
|
|
2099
|
+
};
|
|
2100
|
+
if file_name != name || file_name.is_empty() || file_name.contains(' ') {
|
|
2101
|
+
return false;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
let mut parts = file_name.split('.');
|
|
2105
|
+
let stem = parts.next().unwrap_or_default();
|
|
2106
|
+
let extension = parts.next();
|
|
2107
|
+
if parts.next().is_some() || stem.is_empty() || stem.len() > 8 {
|
|
2108
|
+
return false;
|
|
2109
|
+
}
|
|
2110
|
+
if extension.is_some_and(|extension| extension.is_empty() || extension.len() > 3) {
|
|
2111
|
+
return false;
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
file_name
|
|
2115
|
+
.chars()
|
|
2116
|
+
.all(|char| char.is_ascii_alphanumeric() || matches!(char, '_' | '$' | '~' | '!' | '#'))
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
fn deterministic_guid(kind: &str, parts: &[&str]) -> Uuid {
|
|
2120
|
+
let key = format!("electron-cli:{kind}:{}", parts.join(":"));
|
|
2121
|
+
Uuid::new_v5(&Uuid::NAMESPACE_URL, key.as_bytes())
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
fn msi_guid(uuid: Uuid) -> String {
|
|
2125
|
+
format!("{{{}}}", uuid.hyphenated()).to_ascii_uppercase()
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
fn s(value: impl Into<String>) -> Value {
|
|
2129
|
+
Value::from(value.into())
|
|
2130
|
+
}
|
|
2131
|
+
|
|
1046
2132
|
fn single_line(value: &str) -> String {
|
|
1047
2133
|
value
|
|
1048
2134
|
.chars()
|
|
@@ -1133,7 +2219,7 @@ mod tests {
|
|
|
1133
2219
|
name: None,
|
|
1134
2220
|
platform: None,
|
|
1135
2221
|
arch: None,
|
|
1136
|
-
target: crate::cli::MakeTarget::Zip,
|
|
2222
|
+
target: Some(crate::cli::MakeTarget::Zip),
|
|
1137
2223
|
skip_package: false,
|
|
1138
2224
|
force: false,
|
|
1139
2225
|
dry_run: true,
|
|
@@ -1170,7 +2256,7 @@ mod tests {
|
|
|
1170
2256
|
name: None,
|
|
1171
2257
|
platform: Some("linux".to_string()),
|
|
1172
2258
|
arch: Some("x64".to_string()),
|
|
1173
|
-
target: crate::cli::MakeTarget::Deb,
|
|
2259
|
+
target: Some(crate::cli::MakeTarget::Deb),
|
|
1174
2260
|
skip_package: false,
|
|
1175
2261
|
force: false,
|
|
1176
2262
|
dry_run: true,
|
|
@@ -1204,7 +2290,7 @@ mod tests {
|
|
|
1204
2290
|
name: None,
|
|
1205
2291
|
platform: Some("darwin".to_string()),
|
|
1206
2292
|
arch: Some("arm64".to_string()),
|
|
1207
|
-
target: crate::cli::MakeTarget::Dmg,
|
|
2293
|
+
target: Some(crate::cli::MakeTarget::Dmg),
|
|
1208
2294
|
skip_package: false,
|
|
1209
2295
|
force: false,
|
|
1210
2296
|
dry_run: true,
|
|
@@ -1238,7 +2324,7 @@ mod tests {
|
|
|
1238
2324
|
name: None,
|
|
1239
2325
|
platform: Some("linux".to_string()),
|
|
1240
2326
|
arch: Some("x64".to_string()),
|
|
1241
|
-
target: crate::cli::MakeTarget::Rpm,
|
|
2327
|
+
target: Some(crate::cli::MakeTarget::Rpm),
|
|
1242
2328
|
skip_package: false,
|
|
1243
2329
|
force: false,
|
|
1244
2330
|
dry_run: true,
|
|
@@ -1259,6 +2345,113 @@ mod tests {
|
|
|
1259
2345
|
let _ = fs::remove_dir_all(root);
|
|
1260
2346
|
}
|
|
1261
2347
|
|
|
2348
|
+
#[test]
|
|
2349
|
+
fn builds_make_report_for_msi_target() {
|
|
2350
|
+
let root = unique_temp_dir("msi-plan");
|
|
2351
|
+
write_package_json(&root);
|
|
2352
|
+
write_app_file(&root);
|
|
2353
|
+
write_fake_electron_dist(&root);
|
|
2354
|
+
|
|
2355
|
+
let args = MakeArgs {
|
|
2356
|
+
cwd: root.clone(),
|
|
2357
|
+
out_dir: PathBuf::from("out"),
|
|
2358
|
+
name: None,
|
|
2359
|
+
platform: Some("win32".to_string()),
|
|
2360
|
+
arch: Some("x64".to_string()),
|
|
2361
|
+
target: Some(crate::cli::MakeTarget::Msi),
|
|
2362
|
+
skip_package: false,
|
|
2363
|
+
force: false,
|
|
2364
|
+
dry_run: true,
|
|
2365
|
+
json: true,
|
|
2366
|
+
};
|
|
2367
|
+
let report = build_report(&args).expect("report should build");
|
|
2368
|
+
|
|
2369
|
+
assert_eq!(report.target, "msi");
|
|
2370
|
+
assert!(Path::new(report.artifact.as_str()).ends_with(
|
|
2371
|
+
PathBuf::from("out")
|
|
2372
|
+
.join("make")
|
|
2373
|
+
.join("msi")
|
|
2374
|
+
.join("win32")
|
|
2375
|
+
.join("x64")
|
|
2376
|
+
.join("starter-app-0.1.0-x64.msi")
|
|
2377
|
+
));
|
|
2378
|
+
|
|
2379
|
+
let _ = fs::remove_dir_all(root);
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
#[test]
|
|
2383
|
+
fn builds_make_reports_from_configured_forge_makers() {
|
|
2384
|
+
let root = unique_temp_dir("configured-makers");
|
|
2385
|
+
write_package_json_with_makers(
|
|
2386
|
+
&root,
|
|
2387
|
+
r#"[
|
|
2388
|
+
{"name":"@electron-forge/maker-zip"},
|
|
2389
|
+
{"name":"@electron-forge/maker-deb","platforms":["linux"]},
|
|
2390
|
+
{"name":"@electron-forge/maker-rpm","platforms":["darwin"]},
|
|
2391
|
+
{"name":"@electron-forge/maker-squirrel","platforms":["linux"]}
|
|
2392
|
+
]"#,
|
|
2393
|
+
);
|
|
2394
|
+
write_app_file(&root);
|
|
2395
|
+
write_fake_electron_dist(&root);
|
|
2396
|
+
|
|
2397
|
+
let args = MakeArgs {
|
|
2398
|
+
cwd: root.clone(),
|
|
2399
|
+
out_dir: PathBuf::from("out"),
|
|
2400
|
+
name: None,
|
|
2401
|
+
platform: Some("linux".to_string()),
|
|
2402
|
+
arch: Some("x64".to_string()),
|
|
2403
|
+
target: None,
|
|
2404
|
+
skip_package: false,
|
|
2405
|
+
force: false,
|
|
2406
|
+
dry_run: true,
|
|
2407
|
+
json: true,
|
|
2408
|
+
};
|
|
2409
|
+
let reports = build_reports(&args).expect("reports should build");
|
|
2410
|
+
|
|
2411
|
+
assert_eq!(reports.len(), 2);
|
|
2412
|
+
assert_eq!(reports[0].target(), "zip");
|
|
2413
|
+
assert_eq!(reports[1].target(), "deb");
|
|
2414
|
+
assert!(reports[0]
|
|
2415
|
+
.warnings()
|
|
2416
|
+
.iter()
|
|
2417
|
+
.any(|warning| warning.contains("@electron-forge/maker-squirrel")));
|
|
2418
|
+
|
|
2419
|
+
let _ = fs::remove_dir_all(root);
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
#[test]
|
|
2423
|
+
fn explicit_make_target_overrides_configured_makers() {
|
|
2424
|
+
let root = unique_temp_dir("target-override");
|
|
2425
|
+
write_package_json_with_makers(
|
|
2426
|
+
&root,
|
|
2427
|
+
r#"[{"name":"@electron-forge/maker-zip"},{"name":"@electron-forge/maker-deb"}]"#,
|
|
2428
|
+
);
|
|
2429
|
+
write_app_file(&root);
|
|
2430
|
+
write_fake_electron_dist(&root);
|
|
2431
|
+
|
|
2432
|
+
let args = MakeArgs {
|
|
2433
|
+
cwd: root.clone(),
|
|
2434
|
+
out_dir: PathBuf::from("out"),
|
|
2435
|
+
name: None,
|
|
2436
|
+
platform: Some("win32".to_string()),
|
|
2437
|
+
arch: Some("x64".to_string()),
|
|
2438
|
+
target: Some(crate::cli::MakeTarget::Msi),
|
|
2439
|
+
skip_package: false,
|
|
2440
|
+
force: false,
|
|
2441
|
+
dry_run: true,
|
|
2442
|
+
json: true,
|
|
2443
|
+
};
|
|
2444
|
+
let report = build_report(&args).expect("report should build");
|
|
2445
|
+
|
|
2446
|
+
assert_eq!(report.target(), "msi");
|
|
2447
|
+
assert!(report
|
|
2448
|
+
.warnings()
|
|
2449
|
+
.iter()
|
|
2450
|
+
.all(|warning| !warning.contains("maker-deb")));
|
|
2451
|
+
|
|
2452
|
+
let _ = fs::remove_dir_all(root);
|
|
2453
|
+
}
|
|
2454
|
+
|
|
1262
2455
|
#[test]
|
|
1263
2456
|
fn makes_zip_artifact_after_packaging() {
|
|
1264
2457
|
let root = unique_temp_dir("execute");
|
|
@@ -1272,7 +2465,7 @@ mod tests {
|
|
|
1272
2465
|
name: None,
|
|
1273
2466
|
platform: None,
|
|
1274
2467
|
arch: None,
|
|
1275
|
-
target: crate::cli::MakeTarget::Zip,
|
|
2468
|
+
target: Some(crate::cli::MakeTarget::Zip),
|
|
1276
2469
|
skip_package: false,
|
|
1277
2470
|
force: false,
|
|
1278
2471
|
dry_run: false,
|
|
@@ -1301,6 +2494,44 @@ mod tests {
|
|
|
1301
2494
|
let _ = fs::remove_dir_all(root);
|
|
1302
2495
|
}
|
|
1303
2496
|
|
|
2497
|
+
#[test]
|
|
2498
|
+
fn makes_multiple_configured_artifacts_from_existing_package() {
|
|
2499
|
+
let root = unique_temp_dir("configured-execute");
|
|
2500
|
+
write_package_json_with_makers(
|
|
2501
|
+
&root,
|
|
2502
|
+
r#"[
|
|
2503
|
+
{"name":"@electron-forge/maker-zip","platforms":["win32"]},
|
|
2504
|
+
{"name":"@electron-forge/maker-wix","platforms":["win32"]}
|
|
2505
|
+
]"#,
|
|
2506
|
+
);
|
|
2507
|
+
write_app_file(&root);
|
|
2508
|
+
write_fake_windows_bundle(&root.join("out/starter-app-win32-x64"), "starter-app.exe");
|
|
2509
|
+
|
|
2510
|
+
let args = MakeArgs {
|
|
2511
|
+
cwd: root.clone(),
|
|
2512
|
+
out_dir: PathBuf::from("out"),
|
|
2513
|
+
name: None,
|
|
2514
|
+
platform: Some("win32".to_string()),
|
|
2515
|
+
arch: Some("x64".to_string()),
|
|
2516
|
+
target: None,
|
|
2517
|
+
skip_package: true,
|
|
2518
|
+
force: false,
|
|
2519
|
+
dry_run: false,
|
|
2520
|
+
json: true,
|
|
2521
|
+
};
|
|
2522
|
+
let mut reports = build_reports(&args).expect("reports should build");
|
|
2523
|
+
|
|
2524
|
+
execute_make_reports(&mut reports, &args).expect("configured makers should execute");
|
|
2525
|
+
|
|
2526
|
+
assert_eq!(reports.len(), 2);
|
|
2527
|
+
assert_eq!(reports[0].target(), "zip");
|
|
2528
|
+
assert_eq!(reports[1].target(), "msi");
|
|
2529
|
+
assert!(Path::new(reports[0].artifact.as_str()).exists());
|
|
2530
|
+
assert!(Path::new(reports[1].artifact.as_str()).exists());
|
|
2531
|
+
|
|
2532
|
+
let _ = fs::remove_dir_all(root);
|
|
2533
|
+
}
|
|
2534
|
+
|
|
1304
2535
|
#[test]
|
|
1305
2536
|
fn writes_deb_archive_with_control_and_data_members() {
|
|
1306
2537
|
let root = unique_temp_dir("deb-archive");
|
|
@@ -1314,7 +2545,7 @@ mod tests {
|
|
|
1314
2545
|
name: None,
|
|
1315
2546
|
platform: Some("linux".to_string()),
|
|
1316
2547
|
arch: Some("x64".to_string()),
|
|
1317
|
-
target: crate::cli::MakeTarget::Deb,
|
|
2548
|
+
target: Some(crate::cli::MakeTarget::Deb),
|
|
1318
2549
|
skip_package: false,
|
|
1319
2550
|
force: false,
|
|
1320
2551
|
dry_run: true,
|
|
@@ -1373,7 +2604,7 @@ mod tests {
|
|
|
1373
2604
|
name: None,
|
|
1374
2605
|
platform: Some("darwin".to_string()),
|
|
1375
2606
|
arch: Some("arm64".to_string()),
|
|
1376
|
-
target: crate::cli::MakeTarget::Dmg,
|
|
2607
|
+
target: Some(crate::cli::MakeTarget::Dmg),
|
|
1377
2608
|
skip_package: false,
|
|
1378
2609
|
force: false,
|
|
1379
2610
|
dry_run: true,
|
|
@@ -1441,7 +2672,7 @@ mod tests {
|
|
|
1441
2672
|
name: None,
|
|
1442
2673
|
platform: Some("linux".to_string()),
|
|
1443
2674
|
arch: Some("x64".to_string()),
|
|
1444
|
-
target: crate::cli::MakeTarget::Rpm,
|
|
2675
|
+
target: Some(crate::cli::MakeTarget::Rpm),
|
|
1445
2676
|
skip_package: false,
|
|
1446
2677
|
force: false,
|
|
1447
2678
|
dry_run: true,
|
|
@@ -1487,6 +2718,76 @@ mod tests {
|
|
|
1487
2718
|
let _ = fs::remove_dir_all(root);
|
|
1488
2719
|
}
|
|
1489
2720
|
|
|
2721
|
+
#[test]
|
|
2722
|
+
fn writes_msi_archive_with_database_tables_and_embedded_cabinet() {
|
|
2723
|
+
let root = unique_temp_dir("msi-archive");
|
|
2724
|
+
write_package_json(&root);
|
|
2725
|
+
write_app_file(&root);
|
|
2726
|
+
write_fake_electron_dist(&root);
|
|
2727
|
+
|
|
2728
|
+
let args = MakeArgs {
|
|
2729
|
+
cwd: root.clone(),
|
|
2730
|
+
out_dir: PathBuf::from("out"),
|
|
2731
|
+
name: None,
|
|
2732
|
+
platform: Some("win32".to_string()),
|
|
2733
|
+
arch: Some("x64".to_string()),
|
|
2734
|
+
target: Some(crate::cli::MakeTarget::Msi),
|
|
2735
|
+
skip_package: false,
|
|
2736
|
+
force: false,
|
|
2737
|
+
dry_run: true,
|
|
2738
|
+
json: true,
|
|
2739
|
+
};
|
|
2740
|
+
let report = build_report(&args).expect("report should build");
|
|
2741
|
+
write_fake_windows_bundle(
|
|
2742
|
+
Path::new(report.package.bundle_dir().as_str()),
|
|
2743
|
+
"starter-app.exe",
|
|
2744
|
+
);
|
|
2745
|
+
|
|
2746
|
+
write_msi_archive(&report.package, Path::new(report.artifact.as_str()))
|
|
2747
|
+
.expect("msi should be written");
|
|
2748
|
+
|
|
2749
|
+
let mut installer = msi::open(report.artifact.as_str()).expect("msi should parse");
|
|
2750
|
+
assert_eq!(installer.summary_info().arch(), Some("x64"));
|
|
2751
|
+
assert!(installer.has_table("Property"));
|
|
2752
|
+
assert!(installer.has_table("Directory"));
|
|
2753
|
+
assert!(installer.has_table("File"));
|
|
2754
|
+
assert!(installer.has_table("Media"));
|
|
2755
|
+
assert!(installer.has_stream("app.cab"));
|
|
2756
|
+
|
|
2757
|
+
let properties = msi_rows(&mut installer, "Property");
|
|
2758
|
+
assert!(properties.contains(&vec![
|
|
2759
|
+
Value::from("ProductName"),
|
|
2760
|
+
Value::from("starter-app")
|
|
2761
|
+
]));
|
|
2762
|
+
assert!(properties.contains(&vec![Value::from("ProductVersion"), Value::from("0.1.0")]));
|
|
2763
|
+
|
|
2764
|
+
let files = msi_rows(&mut installer, "File");
|
|
2765
|
+
assert!(files
|
|
2766
|
+
.iter()
|
|
2767
|
+
.any(|row| row[2] == Value::from("F0001.jso|package.json")));
|
|
2768
|
+
assert!(files
|
|
2769
|
+
.iter()
|
|
2770
|
+
.any(|row| row[2] == Value::from("F0002.exe|starter-app.exe")));
|
|
2771
|
+
|
|
2772
|
+
let mut cabinet_bytes = Vec::new();
|
|
2773
|
+
installer
|
|
2774
|
+
.read_stream("app.cab")
|
|
2775
|
+
.expect("cab stream should open")
|
|
2776
|
+
.read_to_end(&mut cabinet_bytes)
|
|
2777
|
+
.expect("cab stream should read");
|
|
2778
|
+
let mut cabinet =
|
|
2779
|
+
cab::Cabinet::new(Cursor::new(cabinet_bytes)).expect("cabinet should parse");
|
|
2780
|
+
let mut package_json = String::new();
|
|
2781
|
+
cabinet
|
|
2782
|
+
.read_file("F0001")
|
|
2783
|
+
.expect("package.json cabinet entry should open")
|
|
2784
|
+
.read_to_string(&mut package_json)
|
|
2785
|
+
.expect("package.json cabinet entry should read");
|
|
2786
|
+
assert_eq!(package_json, "{}");
|
|
2787
|
+
|
|
2788
|
+
let _ = fs::remove_dir_all(root);
|
|
2789
|
+
}
|
|
2790
|
+
|
|
1490
2791
|
#[test]
|
|
1491
2792
|
fn makes_deb_artifact_after_packaging_on_linux() {
|
|
1492
2793
|
if !cfg!(target_os = "linux") {
|
|
@@ -1504,7 +2805,7 @@ mod tests {
|
|
|
1504
2805
|
name: None,
|
|
1505
2806
|
platform: None,
|
|
1506
2807
|
arch: None,
|
|
1507
|
-
target: crate::cli::MakeTarget::Deb,
|
|
2808
|
+
target: Some(crate::cli::MakeTarget::Deb),
|
|
1508
2809
|
skip_package: false,
|
|
1509
2810
|
force: false,
|
|
1510
2811
|
dry_run: false,
|
|
@@ -1536,7 +2837,7 @@ mod tests {
|
|
|
1536
2837
|
name: None,
|
|
1537
2838
|
platform: None,
|
|
1538
2839
|
arch: None,
|
|
1539
|
-
target: crate::cli::MakeTarget::Dmg,
|
|
2840
|
+
target: Some(crate::cli::MakeTarget::Dmg),
|
|
1540
2841
|
skip_package: false,
|
|
1541
2842
|
force: false,
|
|
1542
2843
|
dry_run: false,
|
|
@@ -1568,7 +2869,7 @@ mod tests {
|
|
|
1568
2869
|
name: None,
|
|
1569
2870
|
platform: None,
|
|
1570
2871
|
arch: None,
|
|
1571
|
-
target: crate::cli::MakeTarget::Rpm,
|
|
2872
|
+
target: Some(crate::cli::MakeTarget::Rpm),
|
|
1572
2873
|
skip_package: false,
|
|
1573
2874
|
force: false,
|
|
1574
2875
|
dry_run: false,
|
|
@@ -1591,6 +2892,23 @@ mod tests {
|
|
|
1591
2892
|
.expect("package.json should be written");
|
|
1592
2893
|
}
|
|
1593
2894
|
|
|
2895
|
+
fn write_package_json_with_makers(root: &Path, makers: &str) {
|
|
2896
|
+
fs::write(
|
|
2897
|
+
root.join("package.json"),
|
|
2898
|
+
format!(
|
|
2899
|
+
r#"{{
|
|
2900
|
+
"name":"starter-app",
|
|
2901
|
+
"version":"0.1.0",
|
|
2902
|
+
"license":"MIT",
|
|
2903
|
+
"main":"src/main.js",
|
|
2904
|
+
"devDependencies":{{"electron":"30.0.0"}},
|
|
2905
|
+
"config":{{"forge":{{"makers":{makers}}}}}
|
|
2906
|
+
}}"#
|
|
2907
|
+
),
|
|
2908
|
+
)
|
|
2909
|
+
.expect("package.json with makers should be written");
|
|
2910
|
+
}
|
|
2911
|
+
|
|
1594
2912
|
fn write_app_file(root: &Path) {
|
|
1595
2913
|
fs::create_dir_all(root.join("src")).expect("src should be created");
|
|
1596
2914
|
fs::write(root.join("src/main.js"), "console.log('hello');")
|
|
@@ -1613,6 +2931,15 @@ mod tests {
|
|
|
1613
2931
|
.expect("fake app package should be written");
|
|
1614
2932
|
}
|
|
1615
2933
|
|
|
2934
|
+
fn write_fake_windows_bundle(bundle_dir: &Path, executable_name: &str) {
|
|
2935
|
+
fs::create_dir_all(bundle_dir.join("resources/app"))
|
|
2936
|
+
.expect("fake Windows resources should be created");
|
|
2937
|
+
fs::write(bundle_dir.join(executable_name), "fake exe")
|
|
2938
|
+
.expect("fake Windows executable should be written");
|
|
2939
|
+
fs::write(bundle_dir.join("resources/app/package.json"), "{}")
|
|
2940
|
+
.expect("fake app package should be written");
|
|
2941
|
+
}
|
|
2942
|
+
|
|
1616
2943
|
fn write_fake_electron_dist(root: &Path) {
|
|
1617
2944
|
let dist = root.join("node_modules/electron/dist");
|
|
1618
2945
|
if cfg!(target_os = "macos") {
|
|
@@ -1707,4 +3034,12 @@ mod tests {
|
|
|
1707
3034
|
== path
|
|
1708
3035
|
})
|
|
1709
3036
|
}
|
|
3037
|
+
|
|
3038
|
+
fn msi_rows(installer: &mut msi::Package<File>, table: &str) -> Vec<Vec<Value>> {
|
|
3039
|
+
installer
|
|
3040
|
+
.select_rows(msi::Select::table(table))
|
|
3041
|
+
.expect("msi rows should select")
|
|
3042
|
+
.map(|row| (0..row.len()).map(|index| row[index].clone()).collect())
|
|
3043
|
+
.collect()
|
|
3044
|
+
}
|
|
1710
3045
|
}
|