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,3076 @@
|
|
|
1
|
+
use std::{
|
|
2
|
+
collections::BTreeMap,
|
|
3
|
+
fs,
|
|
4
|
+
fs::File,
|
|
5
|
+
io::{self, BufWriter, Cursor, Write},
|
|
6
|
+
path::{Path, PathBuf},
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
use anyhow::{bail, Context, Result};
|
|
10
|
+
use cab::{CabinetBuilder, CompressionType as CabCompressionType};
|
|
11
|
+
use camino::Utf8PathBuf;
|
|
12
|
+
use fatfs::{Dir as FatDir, FileSystem, FormatVolumeOptions, FsOptions, ReadWriteSeek};
|
|
13
|
+
use flate2::{write::GzEncoder, Compression};
|
|
14
|
+
use fscommon::BufStream;
|
|
15
|
+
use msi::{Column, Insert, Language, Package, PackageType, Value};
|
|
16
|
+
use rpm::{BuildConfig, CompressionType, FileOptions, PackageBuilder};
|
|
17
|
+
use serde::Serialize;
|
|
18
|
+
use serde_json::Value as JsonValue;
|
|
19
|
+
use tar::{Builder as TarBuilder, Header as TarHeader};
|
|
20
|
+
use uuid::Uuid;
|
|
21
|
+
use zip::{write::SimpleFileOptions, CompressionMethod, ZipWriter};
|
|
22
|
+
|
|
23
|
+
use crate::{
|
|
24
|
+
cli::{MakeArgs, MakeTarget, PackageArgs},
|
|
25
|
+
commands::package::{self, PackageReport},
|
|
26
|
+
output,
|
|
27
|
+
project::ProjectSnapshot,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
#[derive(Clone, Debug, Serialize)]
|
|
31
|
+
pub(crate) struct MakeReport {
|
|
32
|
+
package: PackageReport,
|
|
33
|
+
target: String,
|
|
34
|
+
#[serde(skip)]
|
|
35
|
+
target_kind: MakeTarget,
|
|
36
|
+
skip_package: bool,
|
|
37
|
+
dry_run: bool,
|
|
38
|
+
make_dir: Utf8PathBuf,
|
|
39
|
+
artifact: Utf8PathBuf,
|
|
40
|
+
artifact_size: Option<u64>,
|
|
41
|
+
status: MakeStatus,
|
|
42
|
+
warnings: Vec<String>,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[derive(Clone, Copy, Debug, Serialize)]
|
|
46
|
+
#[serde(rename_all = "kebab-case")]
|
|
47
|
+
enum MakeStatus {
|
|
48
|
+
Planned,
|
|
49
|
+
Made,
|
|
50
|
+
}
|
|
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
|
+
|
|
65
|
+
pub fn run(args: MakeArgs) -> Result<()> {
|
|
66
|
+
let mut reports = build_reports(&args)?;
|
|
67
|
+
|
|
68
|
+
if args.dry_run {
|
|
69
|
+
return print_reports(&reports, args.json, MakeStatus::Planned);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
execute_make_reports(&mut reports, &args)?;
|
|
73
|
+
|
|
74
|
+
print_reports(&reports, args.json, MakeStatus::Made)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#[cfg(test)]
|
|
78
|
+
pub(crate) fn build_report(args: &MakeArgs) -> Result<MakeReport> {
|
|
79
|
+
let reports = build_reports(args)?;
|
|
80
|
+
if reports.len() != 1 {
|
|
81
|
+
bail!(
|
|
82
|
+
"Expected one make target, but resolved {}. Pass --target to select one target.",
|
|
83
|
+
reports.len()
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
Ok(reports
|
|
87
|
+
.into_iter()
|
|
88
|
+
.next()
|
|
89
|
+
.expect("length was checked above"))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
pub(crate) fn build_reports(args: &MakeArgs) -> Result<Vec<MakeReport>> {
|
|
93
|
+
let package_args = PackageArgs {
|
|
94
|
+
cwd: args.cwd.clone(),
|
|
95
|
+
out_dir: args.out_dir.clone(),
|
|
96
|
+
name: args.name.clone(),
|
|
97
|
+
platform: args.platform.clone(),
|
|
98
|
+
arch: args.arch.clone(),
|
|
99
|
+
force: args.force,
|
|
100
|
+
dry_run: false,
|
|
101
|
+
json: false,
|
|
102
|
+
};
|
|
103
|
+
let snapshot = crate::project::inspect(&package_args.cwd)?;
|
|
104
|
+
let resolved = resolve_make_targets(&snapshot, args)?;
|
|
105
|
+
let config_warnings = resolved.warnings;
|
|
106
|
+
resolved
|
|
107
|
+
.targets
|
|
108
|
+
.into_iter()
|
|
109
|
+
.map(|target| {
|
|
110
|
+
let package = package::build_report(snapshot.clone(), &package_args)?;
|
|
111
|
+
build_report_for_target(package, target, args, &config_warnings)
|
|
112
|
+
})
|
|
113
|
+
.collect()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fn build_report_for_target(
|
|
117
|
+
package: PackageReport,
|
|
118
|
+
target: MakeTarget,
|
|
119
|
+
args: &MakeArgs,
|
|
120
|
+
config_warnings: &[String],
|
|
121
|
+
) -> Result<MakeReport> {
|
|
122
|
+
let make_dir = Path::new(package.output_dir().as_str())
|
|
123
|
+
.join("make")
|
|
124
|
+
.join(target.as_str())
|
|
125
|
+
.join(package.platform())
|
|
126
|
+
.join(package.arch());
|
|
127
|
+
let artifact = make_artifact_path(&make_dir, &package, target);
|
|
128
|
+
|
|
129
|
+
let mut warnings = package.warnings().to_vec();
|
|
130
|
+
warnings.extend(config_warnings.iter().cloned());
|
|
131
|
+
if matches!(target, MakeTarget::Deb | MakeTarget::Rpm) && package.platform() != "linux" {
|
|
132
|
+
warnings.push(format!(
|
|
133
|
+
"{} maker only supports linux packages; target platform is {}.",
|
|
134
|
+
target.as_str(),
|
|
135
|
+
package.platform()
|
|
136
|
+
));
|
|
137
|
+
}
|
|
138
|
+
if target == MakeTarget::Dmg && package.platform() != "darwin" {
|
|
139
|
+
warnings.push(format!(
|
|
140
|
+
"dmg maker only supports macOS packages; target platform is {}.",
|
|
141
|
+
package.platform()
|
|
142
|
+
));
|
|
143
|
+
}
|
|
144
|
+
if target == MakeTarget::Msi && package.platform() != "win32" {
|
|
145
|
+
warnings.push(format!(
|
|
146
|
+
"msi maker only supports Windows packages; target platform is {}.",
|
|
147
|
+
package.platform()
|
|
148
|
+
));
|
|
149
|
+
}
|
|
150
|
+
if args.skip_package && !Path::new(package.bundle_dir().as_str()).exists() {
|
|
151
|
+
warnings.push(format!(
|
|
152
|
+
"Package output does not exist: {}.",
|
|
153
|
+
package.bundle_dir()
|
|
154
|
+
));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if artifact.exists() && !args.force {
|
|
158
|
+
warnings.push(format!(
|
|
159
|
+
"Make artifact already exists: {}. Use --force to overwrite it.",
|
|
160
|
+
artifact.display()
|
|
161
|
+
));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
Ok(MakeReport {
|
|
165
|
+
package,
|
|
166
|
+
target: target.as_str().to_string(),
|
|
167
|
+
target_kind: target,
|
|
168
|
+
skip_package: args.skip_package,
|
|
169
|
+
dry_run: args.dry_run,
|
|
170
|
+
make_dir: utf8_path(make_dir)?,
|
|
171
|
+
artifact: utf8_path(artifact)?,
|
|
172
|
+
artifact_size: None,
|
|
173
|
+
status: MakeStatus::Planned,
|
|
174
|
+
warnings,
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
struct ConfiguredMaker {
|
|
179
|
+
label: String,
|
|
180
|
+
target: Option<MakeTarget>,
|
|
181
|
+
platforms: Vec<String>,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
fn resolve_make_targets(
|
|
185
|
+
snapshot: &ProjectSnapshot,
|
|
186
|
+
args: &MakeArgs,
|
|
187
|
+
) -> Result<ResolvedMakeTargets> {
|
|
188
|
+
if let Some(target) = args.target {
|
|
189
|
+
return Ok(ResolvedMakeTargets {
|
|
190
|
+
targets: vec![target],
|
|
191
|
+
warnings: Vec::new(),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let platform = args.platform.clone().unwrap_or_else(current_platform_label);
|
|
196
|
+
let makers = configured_makers(snapshot)?;
|
|
197
|
+
let mut warnings = Vec::new();
|
|
198
|
+
let mut targets = Vec::new();
|
|
199
|
+
|
|
200
|
+
for maker in &makers {
|
|
201
|
+
let Some(target) = maker.target else {
|
|
202
|
+
warnings.push(format!(
|
|
203
|
+
"Configured maker is not implemented yet and will be skipped: {}.",
|
|
204
|
+
maker.label
|
|
205
|
+
));
|
|
206
|
+
continue;
|
|
207
|
+
};
|
|
208
|
+
if !maker_applies_to_platform(maker, &platform) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if !targets.contains(&target) {
|
|
212
|
+
targets.push(target);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if targets.is_empty() {
|
|
217
|
+
if makers.is_empty() {
|
|
218
|
+
targets.push(MakeTarget::Zip);
|
|
219
|
+
} else {
|
|
220
|
+
warnings.push(format!(
|
|
221
|
+
"No supported configured makers apply to {platform}; defaulting to zip. Pass --target to override."
|
|
222
|
+
));
|
|
223
|
+
targets.push(MakeTarget::Zip);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
Ok(ResolvedMakeTargets { targets, warnings })
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
fn configured_makers(snapshot: &ProjectSnapshot) -> Result<Vec<ConfiguredMaker>> {
|
|
231
|
+
let project_config = crate::forge_config::read(snapshot)?;
|
|
232
|
+
|
|
233
|
+
let mut makers = Vec::new();
|
|
234
|
+
for value in [
|
|
235
|
+
project_config.forge().and_then(|forge| forge.get("makers")),
|
|
236
|
+
project_config
|
|
237
|
+
.electron_cli()
|
|
238
|
+
.and_then(|config| config.get("makers")),
|
|
239
|
+
]
|
|
240
|
+
.into_iter()
|
|
241
|
+
.flatten()
|
|
242
|
+
{
|
|
243
|
+
makers.extend(parse_maker_list(value));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
Ok(makers)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
fn parse_maker_list(value: &JsonValue) -> Vec<ConfiguredMaker> {
|
|
250
|
+
match value {
|
|
251
|
+
JsonValue::Array(values) => values.iter().filter_map(parse_maker).collect(),
|
|
252
|
+
_ => Vec::new(),
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
fn parse_maker(value: &JsonValue) -> Option<ConfiguredMaker> {
|
|
257
|
+
match value {
|
|
258
|
+
JsonValue::String(label) => Some(ConfiguredMaker {
|
|
259
|
+
label: label.clone(),
|
|
260
|
+
target: maker_target(label),
|
|
261
|
+
platforms: Vec::new(),
|
|
262
|
+
}),
|
|
263
|
+
JsonValue::Object(object) => {
|
|
264
|
+
let label = object
|
|
265
|
+
.get("name")
|
|
266
|
+
.or_else(|| object.get("target"))
|
|
267
|
+
.or_else(|| object.get("maker"))
|
|
268
|
+
.and_then(JsonValue::as_str)?
|
|
269
|
+
.to_string();
|
|
270
|
+
Some(ConfiguredMaker {
|
|
271
|
+
target: maker_target(&label),
|
|
272
|
+
platforms: string_values(object.get("platforms")),
|
|
273
|
+
label,
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
_ => None,
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
fn maker_target(label: &str) -> Option<MakeTarget> {
|
|
281
|
+
let label = label.trim().to_ascii_lowercase();
|
|
282
|
+
let compact = label
|
|
283
|
+
.trim_start_matches("@electron-forge/")
|
|
284
|
+
.trim_start_matches("electron-forge-")
|
|
285
|
+
.trim_start_matches("maker-");
|
|
286
|
+
|
|
287
|
+
if matches!(compact, "zip" | "@electron-forge/maker-zip")
|
|
288
|
+
|| label.ends_with("/maker-zip")
|
|
289
|
+
|| label.ends_with("maker-zip")
|
|
290
|
+
{
|
|
291
|
+
Some(MakeTarget::Zip)
|
|
292
|
+
} else if compact == "dmg" || label.ends_with("/maker-dmg") || label.ends_with("maker-dmg") {
|
|
293
|
+
Some(MakeTarget::Dmg)
|
|
294
|
+
} else if compact == "deb" || label.ends_with("/maker-deb") || label.ends_with("maker-deb") {
|
|
295
|
+
Some(MakeTarget::Deb)
|
|
296
|
+
} else if compact == "rpm" || label.ends_with("/maker-rpm") || label.ends_with("maker-rpm") {
|
|
297
|
+
Some(MakeTarget::Rpm)
|
|
298
|
+
} else if matches!(compact, "msi" | "wix")
|
|
299
|
+
|| label.ends_with("/maker-wix")
|
|
300
|
+
|| label.ends_with("maker-wix")
|
|
301
|
+
{
|
|
302
|
+
Some(MakeTarget::Msi)
|
|
303
|
+
} else {
|
|
304
|
+
None
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
fn maker_applies_to_platform(maker: &ConfiguredMaker, platform: &str) -> bool {
|
|
309
|
+
maker.platforms.is_empty()
|
|
310
|
+
|| maker
|
|
311
|
+
.platforms
|
|
312
|
+
.iter()
|
|
313
|
+
.any(|configured| configured == platform || configured == "*")
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
fn string_values(value: Option<&JsonValue>) -> Vec<String> {
|
|
317
|
+
match value {
|
|
318
|
+
Some(JsonValue::String(value)) => vec![value.clone()],
|
|
319
|
+
Some(JsonValue::Array(values)) => values
|
|
320
|
+
.iter()
|
|
321
|
+
.filter_map(JsonValue::as_str)
|
|
322
|
+
.map(ToOwned::to_owned)
|
|
323
|
+
.collect(),
|
|
324
|
+
_ => Vec::new(),
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
fn current_platform_label() -> String {
|
|
329
|
+
if cfg!(target_os = "macos") {
|
|
330
|
+
"darwin".to_string()
|
|
331
|
+
} else if cfg!(target_os = "windows") {
|
|
332
|
+
"win32".to_string()
|
|
333
|
+
} else {
|
|
334
|
+
"linux".to_string()
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
#[cfg(test)]
|
|
339
|
+
pub(crate) fn execute_make(report: &mut MakeReport, args: &MakeArgs) -> Result<()> {
|
|
340
|
+
ensure_package_ready(std::slice::from_mut(report), args)?;
|
|
341
|
+
execute_make_artifact(report, args)?;
|
|
342
|
+
Ok(())
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
pub(crate) fn execute_make_reports(reports: &mut [MakeReport], args: &MakeArgs) -> Result<()> {
|
|
346
|
+
if reports.is_empty() {
|
|
347
|
+
bail!("No make targets were resolved.");
|
|
348
|
+
}
|
|
349
|
+
ensure_package_ready(reports, args)?;
|
|
350
|
+
for report in reports {
|
|
351
|
+
execute_make_artifact(report, args)?;
|
|
352
|
+
report.mark_made()?;
|
|
353
|
+
}
|
|
354
|
+
Ok(())
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
fn ensure_package_ready(reports: &mut [MakeReport], args: &MakeArgs) -> Result<()> {
|
|
358
|
+
let first = reports
|
|
359
|
+
.first_mut()
|
|
360
|
+
.context("No make targets were resolved.")?;
|
|
361
|
+
if !args.skip_package {
|
|
362
|
+
package::execute_package(&first.package, args.force)?;
|
|
363
|
+
for report in reports {
|
|
364
|
+
report.package.mark_packaged();
|
|
365
|
+
}
|
|
366
|
+
} else if !Path::new(first.package.bundle_dir().as_str()).exists() {
|
|
367
|
+
bail!(
|
|
368
|
+
"Package output does not exist: {}. Run without --skip-package or run electron-cli package first.",
|
|
369
|
+
first.package.bundle_dir()
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
Ok(())
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
fn execute_make_artifact(report: &mut MakeReport, args: &MakeArgs) -> Result<()> {
|
|
377
|
+
let artifact = Path::new(report.artifact.as_str());
|
|
378
|
+
if artifact.exists() {
|
|
379
|
+
if args.force {
|
|
380
|
+
fs::remove_file(artifact)
|
|
381
|
+
.with_context(|| format!("Could not remove {}", artifact.display()))?;
|
|
382
|
+
} else {
|
|
383
|
+
bail!(
|
|
384
|
+
"Make artifact already exists: {}. Use --force to overwrite it.",
|
|
385
|
+
artifact.display()
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
fs::create_dir_all(report.make_dir.as_str())
|
|
391
|
+
.with_context(|| format!("Could not create {}", report.make_dir))?;
|
|
392
|
+
match report.target_kind {
|
|
393
|
+
MakeTarget::Zip => {
|
|
394
|
+
write_zip_archive(Path::new(report.package.bundle_dir().as_str()), artifact)?
|
|
395
|
+
}
|
|
396
|
+
MakeTarget::Deb => write_deb_archive(&report.package, artifact)?,
|
|
397
|
+
MakeTarget::Dmg => write_dmg_archive(&report.package, artifact)?,
|
|
398
|
+
MakeTarget::Msi => write_msi_archive(&report.package, artifact)?,
|
|
399
|
+
MakeTarget::Rpm => write_rpm_archive(&report.package, artifact)?,
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
Ok(())
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
fn print_report(report: &MakeReport, json: bool) -> Result<()> {
|
|
406
|
+
if json {
|
|
407
|
+
return output::json(report);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
println!("electron-cli make");
|
|
411
|
+
println!();
|
|
412
|
+
println!("Project");
|
|
413
|
+
println!(" root: {}", report.package.project().root);
|
|
414
|
+
match report.package.project().package_label() {
|
|
415
|
+
Some(label) => println!(" package: {label}"),
|
|
416
|
+
None => println!(" package: not found"),
|
|
417
|
+
}
|
|
418
|
+
println!(" app name: {}", report.package.app_name());
|
|
419
|
+
println!(
|
|
420
|
+
" target: {} {} {}",
|
|
421
|
+
report.target,
|
|
422
|
+
report.package.platform(),
|
|
423
|
+
report.package.arch()
|
|
424
|
+
);
|
|
425
|
+
println!(" status: {}", report.status.as_str());
|
|
426
|
+
|
|
427
|
+
println!();
|
|
428
|
+
println!("Artifact");
|
|
429
|
+
println!(" {}", report.artifact);
|
|
430
|
+
if let Some(size) = report.artifact_size {
|
|
431
|
+
println!(" size: {size} bytes");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if !report.warnings.is_empty() {
|
|
435
|
+
println!();
|
|
436
|
+
println!("Warnings");
|
|
437
|
+
for warning in &report.warnings {
|
|
438
|
+
println!(" {warning}");
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
Ok(())
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
fn print_reports(reports: &[MakeReport], json: bool, status: MakeStatus) -> Result<()> {
|
|
446
|
+
if reports.len() == 1 {
|
|
447
|
+
return print_report(&reports[0], json);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
let warnings = combined_warnings(reports);
|
|
451
|
+
if json {
|
|
452
|
+
return output::json(&MakeRunReport {
|
|
453
|
+
targets: reports,
|
|
454
|
+
dry_run: reports.iter().any(|report| report.dry_run),
|
|
455
|
+
status,
|
|
456
|
+
warnings,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
println!("electron-cli make");
|
|
461
|
+
println!();
|
|
462
|
+
if let Some(first) = reports.first() {
|
|
463
|
+
println!("Project");
|
|
464
|
+
println!(" root: {}", first.package.project().root);
|
|
465
|
+
match first.package.project().package_label() {
|
|
466
|
+
Some(label) => println!(" package: {label}"),
|
|
467
|
+
None => println!(" package: not found"),
|
|
468
|
+
}
|
|
469
|
+
println!(" app name: {}", first.package.app_name());
|
|
470
|
+
println!(
|
|
471
|
+
" target platform: {} {}",
|
|
472
|
+
first.package.platform(),
|
|
473
|
+
first.package.arch()
|
|
474
|
+
);
|
|
475
|
+
println!(" status: {}", status.as_str());
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
println!();
|
|
479
|
+
println!("Artifacts");
|
|
480
|
+
for report in reports {
|
|
481
|
+
println!(" {}: {}", report.target, report.artifact);
|
|
482
|
+
if let Some(size) = report.artifact_size {
|
|
483
|
+
println!(" size: {size} bytes");
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if !warnings.is_empty() {
|
|
488
|
+
println!();
|
|
489
|
+
println!("Warnings");
|
|
490
|
+
for warning in warnings {
|
|
491
|
+
println!(" {warning}");
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
Ok(())
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
fn combined_warnings(reports: &[MakeReport]) -> Vec<String> {
|
|
499
|
+
let mut warnings = Vec::new();
|
|
500
|
+
for warning in reports.iter().flat_map(|report| report.warnings()) {
|
|
501
|
+
if !warnings.contains(warning) {
|
|
502
|
+
warnings.push(warning.clone());
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
warnings
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
fn make_artifact_path(make_dir: &Path, package: &PackageReport, target: MakeTarget) -> PathBuf {
|
|
509
|
+
match target {
|
|
510
|
+
MakeTarget::Zip => make_dir.join(format!(
|
|
511
|
+
"{}-{}-{}.zip",
|
|
512
|
+
package.artifact_stem(),
|
|
513
|
+
package.platform(),
|
|
514
|
+
package.arch()
|
|
515
|
+
)),
|
|
516
|
+
MakeTarget::Deb => make_dir.join(format!(
|
|
517
|
+
"{}_{}_{}.deb",
|
|
518
|
+
debian_package_name(&package.artifact_stem()),
|
|
519
|
+
debian_version(package.project().version.as_deref()),
|
|
520
|
+
debian_arch(package.arch())
|
|
521
|
+
)),
|
|
522
|
+
MakeTarget::Dmg => make_dir.join(format!(
|
|
523
|
+
"{}-{}-{}.dmg",
|
|
524
|
+
package.artifact_stem(),
|
|
525
|
+
dmg_version(package.project().version.as_deref()),
|
|
526
|
+
package.arch()
|
|
527
|
+
)),
|
|
528
|
+
MakeTarget::Msi => make_dir.join(format!(
|
|
529
|
+
"{}-{}-{}.msi",
|
|
530
|
+
package.artifact_stem(),
|
|
531
|
+
windows_artifact_version(package.project().version.as_deref()),
|
|
532
|
+
windows_arch(package.arch())
|
|
533
|
+
)),
|
|
534
|
+
MakeTarget::Rpm => make_dir.join(format!(
|
|
535
|
+
"{}-{}-1.{}.rpm",
|
|
536
|
+
rpm_package_name(&package.artifact_stem()),
|
|
537
|
+
rpm_version(package.project().version.as_deref()),
|
|
538
|
+
rpm_arch(package.arch())
|
|
539
|
+
)),
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
fn write_zip_archive(source: &Path, artifact: &Path) -> Result<()> {
|
|
544
|
+
if !source.exists() {
|
|
545
|
+
bail!("Package output does not exist: {}", source.display());
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
let parent = artifact
|
|
549
|
+
.parent()
|
|
550
|
+
.with_context(|| format!("Artifact path has no parent: {}", artifact.display()))?;
|
|
551
|
+
fs::create_dir_all(parent).with_context(|| format!("Could not create {}", parent.display()))?;
|
|
552
|
+
|
|
553
|
+
let file = File::create(artifact)
|
|
554
|
+
.with_context(|| format!("Could not create {}", artifact.display()))?;
|
|
555
|
+
let mut writer = ZipWriter::new(BufWriter::new(file));
|
|
556
|
+
let base = source
|
|
557
|
+
.parent()
|
|
558
|
+
.with_context(|| format!("Package output has no parent: {}", source.display()))?;
|
|
559
|
+
|
|
560
|
+
add_path_to_zip(source, base, &mut writer)?;
|
|
561
|
+
writer
|
|
562
|
+
.finish()
|
|
563
|
+
.with_context(|| format!("Could not finish {}", artifact.display()))?;
|
|
564
|
+
|
|
565
|
+
Ok(())
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
fn write_dmg_archive(package: &PackageReport, artifact: &Path) -> Result<()> {
|
|
569
|
+
if package.platform() != "darwin" {
|
|
570
|
+
bail!(
|
|
571
|
+
"DMG maker only supports macOS packages. Requested {}.",
|
|
572
|
+
package.platform()
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
let source = Path::new(package.bundle_dir().as_str());
|
|
577
|
+
if !source.exists() {
|
|
578
|
+
bail!("Package output does not exist: {}", source.display());
|
|
579
|
+
}
|
|
580
|
+
if source.extension().and_then(|extension| extension.to_str()) != Some("app") {
|
|
581
|
+
bail!(
|
|
582
|
+
"DMG maker expected a macOS .app bundle: {}",
|
|
583
|
+
source.display()
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
let parent = artifact
|
|
588
|
+
.parent()
|
|
589
|
+
.with_context(|| format!("Artifact path has no parent: {}", artifact.display()))?;
|
|
590
|
+
fs::create_dir_all(parent).with_context(|| format!("Could not create {}", parent.display()))?;
|
|
591
|
+
|
|
592
|
+
let volume_label = fat_volume_label(package.app_name());
|
|
593
|
+
let fat32 = create_dmg_fat32(source, &volume_label)?;
|
|
594
|
+
apple_dmg::DmgWriter::create(artifact)
|
|
595
|
+
.with_context(|| format!("Could not create {}", artifact.display()))?
|
|
596
|
+
.create_fat32(&fat32)
|
|
597
|
+
.with_context(|| format!("Could not write {}", artifact.display()))
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const DMG_SECTOR_SIZE: u64 = 512;
|
|
601
|
+
const DMG_MIN_BYTES: u64 = 64 * 1024 * 1024;
|
|
602
|
+
const DMG_SECTOR_ALIGNMENT: u64 = 2048;
|
|
603
|
+
|
|
604
|
+
fn create_dmg_fat32(app_bundle: &Path, volume_label: &[u8; 11]) -> Result<Vec<u8>> {
|
|
605
|
+
let total_sectors = dmg_total_sectors(app_bundle)?;
|
|
606
|
+
let mut fat32 = vec![0; total_sectors as usize * DMG_SECTOR_SIZE as usize];
|
|
607
|
+
|
|
608
|
+
{
|
|
609
|
+
let volume_options = FormatVolumeOptions::new()
|
|
610
|
+
.volume_label(*volume_label)
|
|
611
|
+
.bytes_per_sector(DMG_SECTOR_SIZE as u16)
|
|
612
|
+
.total_sectors(total_sectors);
|
|
613
|
+
let mut disk = BufStream::new(Cursor::new(&mut fat32));
|
|
614
|
+
fatfs::format_volume(&mut disk, volume_options)
|
|
615
|
+
.context("Could not format DMG FAT32 volume")?;
|
|
616
|
+
drop(disk);
|
|
617
|
+
|
|
618
|
+
let disk = BufStream::new(Cursor::new(&mut fat32));
|
|
619
|
+
let fs =
|
|
620
|
+
FileSystem::new(disk, FsOptions::new()).context("Could not open DMG FAT32 volume")?;
|
|
621
|
+
let root = fs.root_dir();
|
|
622
|
+
let app_name = utf8_file_name(app_bundle)?;
|
|
623
|
+
let app_dir = root
|
|
624
|
+
.create_dir(app_name)
|
|
625
|
+
.with_context(|| format!("Could not add {app_name} to DMG"))?;
|
|
626
|
+
add_directory_to_fat(app_bundle, &app_dir)
|
|
627
|
+
.with_context(|| format!("Could not add {} to DMG", app_bundle.display()))?;
|
|
628
|
+
write_fat_symlink(&root, "Applications", "/Applications")
|
|
629
|
+
.context("Could not add Applications link to DMG")?;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
Ok(fat32)
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
fn dmg_total_sectors(app_bundle: &Path) -> Result<u32> {
|
|
636
|
+
let stats = directory_stats(app_bundle)?;
|
|
637
|
+
let cluster_slack_estimate =
|
|
638
|
+
stats.files.saturating_mul(16 * 1024) + stats.directories.saturating_mul(4096);
|
|
639
|
+
let payload_estimate = stats
|
|
640
|
+
.bytes
|
|
641
|
+
.saturating_add(cluster_slack_estimate)
|
|
642
|
+
.saturating_add(16 * 1024 * 1024);
|
|
643
|
+
let required_bytes = payload_estimate
|
|
644
|
+
.saturating_add(payload_estimate / 3)
|
|
645
|
+
.max(DMG_MIN_BYTES);
|
|
646
|
+
let sectors = required_bytes.div_ceil(DMG_SECTOR_SIZE);
|
|
647
|
+
let aligned_sectors = sectors.div_ceil(DMG_SECTOR_ALIGNMENT) * DMG_SECTOR_ALIGNMENT;
|
|
648
|
+
u32::try_from(aligned_sectors).context("DMG contents are too large for a FAT32 image")
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
#[derive(Default)]
|
|
652
|
+
struct DirectoryStats {
|
|
653
|
+
bytes: u64,
|
|
654
|
+
files: u64,
|
|
655
|
+
directories: u64,
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
fn directory_stats(path: &Path) -> Result<DirectoryStats> {
|
|
659
|
+
let metadata =
|
|
660
|
+
fs::symlink_metadata(path).with_context(|| format!("Could not stat {}", path.display()))?;
|
|
661
|
+
if metadata.is_file() {
|
|
662
|
+
return Ok(DirectoryStats {
|
|
663
|
+
bytes: metadata.len(),
|
|
664
|
+
files: 1,
|
|
665
|
+
directories: 0,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
if metadata.file_type().is_symlink() {
|
|
669
|
+
return Ok(DirectoryStats {
|
|
670
|
+
bytes: read_link_lossy(path)?.len() as u64,
|
|
671
|
+
files: 1,
|
|
672
|
+
directories: 0,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
if !metadata.is_dir() {
|
|
676
|
+
return Ok(DirectoryStats::default());
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
let mut stats = DirectoryStats {
|
|
680
|
+
bytes: 0,
|
|
681
|
+
files: 0,
|
|
682
|
+
directories: 1,
|
|
683
|
+
};
|
|
684
|
+
for entry in fs::read_dir(path).with_context(|| format!("Could not read {}", path.display()))? {
|
|
685
|
+
let entry = entry?;
|
|
686
|
+
let child = directory_stats(&entry.path())?;
|
|
687
|
+
stats.bytes = stats.bytes.saturating_add(child.bytes);
|
|
688
|
+
stats.files = stats.files.saturating_add(child.files);
|
|
689
|
+
stats.directories = stats.directories.saturating_add(child.directories);
|
|
690
|
+
}
|
|
691
|
+
Ok(stats)
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
fn add_directory_to_fat<T: ReadWriteSeek>(
|
|
695
|
+
source: &Path,
|
|
696
|
+
destination: &FatDir<'_, T>,
|
|
697
|
+
) -> Result<()> {
|
|
698
|
+
let mut entries = fs::read_dir(source)
|
|
699
|
+
.with_context(|| format!("Could not read {}", source.display()))?
|
|
700
|
+
.collect::<Result<Vec<_>, io::Error>>()?;
|
|
701
|
+
entries.sort_by_key(|entry| entry.path());
|
|
702
|
+
|
|
703
|
+
for entry in entries {
|
|
704
|
+
let source_path = entry.path();
|
|
705
|
+
let file_name = utf8_file_name(&source_path)?;
|
|
706
|
+
let metadata = fs::symlink_metadata(&source_path)
|
|
707
|
+
.with_context(|| format!("Could not stat {}", source_path.display()))?;
|
|
708
|
+
|
|
709
|
+
if metadata.is_dir() {
|
|
710
|
+
let child = destination
|
|
711
|
+
.create_dir(file_name)
|
|
712
|
+
.with_context(|| format!("Could not create DMG directory {file_name}"))?;
|
|
713
|
+
add_directory_to_fat(&source_path, &child)?;
|
|
714
|
+
} else if metadata.file_type().is_symlink() {
|
|
715
|
+
let target = read_link_lossy(&source_path)?;
|
|
716
|
+
write_fat_symlink(destination, file_name, &target)?;
|
|
717
|
+
} else if metadata.is_file() {
|
|
718
|
+
let mut source_file = File::open(&source_path)
|
|
719
|
+
.with_context(|| format!("Could not open {}", source_path.display()))?;
|
|
720
|
+
let mut destination_file = destination
|
|
721
|
+
.create_file(file_name)
|
|
722
|
+
.with_context(|| format!("Could not create DMG file {file_name}"))?;
|
|
723
|
+
io::copy(&mut source_file, &mut destination_file)
|
|
724
|
+
.with_context(|| format!("Could not write DMG file {file_name}"))?;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
Ok(())
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
fn write_fat_symlink<T: ReadWriteSeek>(
|
|
732
|
+
directory: &FatDir<'_, T>,
|
|
733
|
+
name: &str,
|
|
734
|
+
target: &str,
|
|
735
|
+
) -> Result<()> {
|
|
736
|
+
let bytes = fat_symlink_bytes(target)?;
|
|
737
|
+
let mut file = directory
|
|
738
|
+
.create_file(name)
|
|
739
|
+
.with_context(|| format!("Could not create DMG symlink {name}"))?;
|
|
740
|
+
file.write_all(&bytes)
|
|
741
|
+
.with_context(|| format!("Could not write DMG symlink {name}"))
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
fn fat_symlink_bytes(target: &str) -> Result<Vec<u8>> {
|
|
745
|
+
let mut bytes = format!(
|
|
746
|
+
"XSym\n{:04}\n{:x}\n{}\n",
|
|
747
|
+
target.len(),
|
|
748
|
+
md5::compute(target.as_bytes()),
|
|
749
|
+
target
|
|
750
|
+
)
|
|
751
|
+
.into_bytes();
|
|
752
|
+
anyhow::ensure!(bytes.len() <= 1067, "Symlink target is too long: {target}");
|
|
753
|
+
bytes.resize(1067, b' ');
|
|
754
|
+
Ok(bytes)
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
fn fat_volume_label(name: &str) -> [u8; 11] {
|
|
758
|
+
let mut label = [b' '; 11];
|
|
759
|
+
let sanitized = name
|
|
760
|
+
.to_ascii_uppercase()
|
|
761
|
+
.bytes()
|
|
762
|
+
.filter(|byte| byte.is_ascii_alphanumeric())
|
|
763
|
+
.take(11)
|
|
764
|
+
.collect::<Vec<_>>();
|
|
765
|
+
if sanitized.is_empty() {
|
|
766
|
+
label[..3].copy_from_slice(b"APP");
|
|
767
|
+
} else {
|
|
768
|
+
label[..sanitized.len()].copy_from_slice(&sanitized);
|
|
769
|
+
}
|
|
770
|
+
label
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
fn read_link_lossy(path: &Path) -> Result<String> {
|
|
774
|
+
Ok(fs::read_link(path)
|
|
775
|
+
.with_context(|| format!("Could not read link {}", path.display()))?
|
|
776
|
+
.to_string_lossy()
|
|
777
|
+
.to_string())
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
fn utf8_file_name(path: &Path) -> Result<&str> {
|
|
781
|
+
path.file_name()
|
|
782
|
+
.and_then(|file_name| file_name.to_str())
|
|
783
|
+
.with_context(|| format!("Path has no UTF-8 file name: {}", path.display()))
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
fn add_path_to_zip(
|
|
787
|
+
path: &Path,
|
|
788
|
+
base: &Path,
|
|
789
|
+
writer: &mut ZipWriter<BufWriter<File>>,
|
|
790
|
+
) -> Result<()> {
|
|
791
|
+
let metadata =
|
|
792
|
+
fs::metadata(path).with_context(|| format!("Could not stat {}", path.display()))?;
|
|
793
|
+
let relative_path = zip_relative_path(path, base)?;
|
|
794
|
+
|
|
795
|
+
if metadata.is_dir() {
|
|
796
|
+
if !relative_path.is_empty() {
|
|
797
|
+
let directory_name = format!("{relative_path}/");
|
|
798
|
+
writer
|
|
799
|
+
.add_directory(directory_name, directory_options(&metadata))
|
|
800
|
+
.with_context(|| format!("Could not add {} to archive", path.display()))?;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
let mut entries = fs::read_dir(path)
|
|
804
|
+
.with_context(|| format!("Could not read {}", path.display()))?
|
|
805
|
+
.collect::<Result<Vec<_>, io::Error>>()?;
|
|
806
|
+
entries.sort_by_key(|entry| entry.path());
|
|
807
|
+
|
|
808
|
+
for entry in entries {
|
|
809
|
+
add_path_to_zip(&entry.path(), base, writer)?;
|
|
810
|
+
}
|
|
811
|
+
} else {
|
|
812
|
+
writer
|
|
813
|
+
.start_file(relative_path, file_options(&metadata))
|
|
814
|
+
.with_context(|| format!("Could not add {} to archive", path.display()))?;
|
|
815
|
+
let mut file =
|
|
816
|
+
File::open(path).with_context(|| format!("Could not open {}", path.display()))?;
|
|
817
|
+
io::copy(&mut file, writer)
|
|
818
|
+
.with_context(|| format!("Could not write {} to archive", path.display()))?;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
Ok(())
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
fn zip_relative_path(path: &Path, base: &Path) -> Result<String> {
|
|
825
|
+
let relative = path.strip_prefix(base).with_context(|| {
|
|
826
|
+
format!(
|
|
827
|
+
"Could not make {} relative to {}",
|
|
828
|
+
path.display(),
|
|
829
|
+
base.display()
|
|
830
|
+
)
|
|
831
|
+
})?;
|
|
832
|
+
Ok(relative
|
|
833
|
+
.components()
|
|
834
|
+
.map(|component| component.as_os_str().to_string_lossy())
|
|
835
|
+
.collect::<Vec<_>>()
|
|
836
|
+
.join("/"))
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
fn file_options(metadata: &fs::Metadata) -> SimpleFileOptions {
|
|
840
|
+
SimpleFileOptions::default()
|
|
841
|
+
.compression_method(CompressionMethod::Deflated)
|
|
842
|
+
.unix_permissions(unix_mode(metadata, 0o644))
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
fn directory_options(metadata: &fs::Metadata) -> SimpleFileOptions {
|
|
846
|
+
SimpleFileOptions::default()
|
|
847
|
+
.compression_method(CompressionMethod::Stored)
|
|
848
|
+
.unix_permissions(unix_mode(metadata, 0o755))
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
fn write_deb_archive(package: &PackageReport, artifact: &Path) -> Result<()> {
|
|
852
|
+
if package.platform() != "linux" {
|
|
853
|
+
bail!(
|
|
854
|
+
"Deb maker only supports linux packages. Requested {}.",
|
|
855
|
+
package.platform()
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
let source = Path::new(package.bundle_dir().as_str());
|
|
860
|
+
if !source.exists() {
|
|
861
|
+
bail!("Package output does not exist: {}", source.display());
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
let parent = artifact
|
|
865
|
+
.parent()
|
|
866
|
+
.with_context(|| format!("Artifact path has no parent: {}", artifact.display()))?;
|
|
867
|
+
fs::create_dir_all(parent).with_context(|| format!("Could not create {}", parent.display()))?;
|
|
868
|
+
|
|
869
|
+
let deb_package = debian_package_name(&package.artifact_stem());
|
|
870
|
+
let version = debian_version(package.project().version.as_deref());
|
|
871
|
+
let arch = debian_arch(package.arch());
|
|
872
|
+
let installed_size = directory_size(source)?.div_ceil(1024).max(1);
|
|
873
|
+
let control = debian_control_file(package, &deb_package, &version, &arch, installed_size);
|
|
874
|
+
let control_tar =
|
|
875
|
+
gzip_tar(|builder| append_bytes_to_tar(builder, "./control", control.as_bytes(), 0o644))?;
|
|
876
|
+
let data_tar = gzip_tar(|builder| append_deb_data_tar(builder, package, source, &deb_package))?;
|
|
877
|
+
|
|
878
|
+
write_ar_archive(
|
|
879
|
+
artifact,
|
|
880
|
+
&[
|
|
881
|
+
ArMember {
|
|
882
|
+
name: "debian-binary",
|
|
883
|
+
mode: 0o100644,
|
|
884
|
+
data: b"2.0\n".to_vec(),
|
|
885
|
+
},
|
|
886
|
+
ArMember {
|
|
887
|
+
name: "control.tar.gz",
|
|
888
|
+
mode: 0o100644,
|
|
889
|
+
data: control_tar,
|
|
890
|
+
},
|
|
891
|
+
ArMember {
|
|
892
|
+
name: "data.tar.gz",
|
|
893
|
+
mode: 0o100644,
|
|
894
|
+
data: data_tar,
|
|
895
|
+
},
|
|
896
|
+
],
|
|
897
|
+
)
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
fn write_rpm_archive(package: &PackageReport, artifact: &Path) -> Result<()> {
|
|
901
|
+
if package.platform() != "linux" {
|
|
902
|
+
bail!(
|
|
903
|
+
"RPM maker only supports linux packages. Requested {}.",
|
|
904
|
+
package.platform()
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
let source = Path::new(package.bundle_dir().as_str());
|
|
909
|
+
if !source.exists() {
|
|
910
|
+
bail!("Package output does not exist: {}", source.display());
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
let parent = artifact
|
|
914
|
+
.parent()
|
|
915
|
+
.with_context(|| format!("Artifact path has no parent: {}", artifact.display()))?;
|
|
916
|
+
fs::create_dir_all(parent).with_context(|| format!("Could not create {}", parent.display()))?;
|
|
917
|
+
|
|
918
|
+
let rpm_package = rpm_package_name(&package.artifact_stem());
|
|
919
|
+
let version = rpm_version(package.project().version.as_deref());
|
|
920
|
+
let arch = rpm_arch(package.arch());
|
|
921
|
+
let executable = format!("/opt/{rpm_package}/{}", package.executable_name());
|
|
922
|
+
let mut builder = PackageBuilder::new(
|
|
923
|
+
&rpm_package,
|
|
924
|
+
&version,
|
|
925
|
+
package
|
|
926
|
+
.project()
|
|
927
|
+
.license
|
|
928
|
+
.as_deref()
|
|
929
|
+
.unwrap_or("LicenseRef-unknown"),
|
|
930
|
+
&arch,
|
|
931
|
+
&single_line(package.app_name()),
|
|
932
|
+
);
|
|
933
|
+
builder
|
|
934
|
+
.using_config(
|
|
935
|
+
BuildConfig::v4()
|
|
936
|
+
.compression(CompressionType::Gzip)
|
|
937
|
+
.reserved_space(None)
|
|
938
|
+
.source_date(0),
|
|
939
|
+
)
|
|
940
|
+
.release("1")
|
|
941
|
+
.vendor("electron-cli")
|
|
942
|
+
.packager("electron-cli")
|
|
943
|
+
.description(format!(
|
|
944
|
+
"{} packaged by electron-cli.",
|
|
945
|
+
single_line(package.app_name())
|
|
946
|
+
))
|
|
947
|
+
.default_file_attrs(None, Some("root".to_string()), Some("root".to_string()))
|
|
948
|
+
.default_dir_attrs(None, Some("root".to_string()), Some("root".to_string()));
|
|
949
|
+
|
|
950
|
+
for directory in [
|
|
951
|
+
"/opt",
|
|
952
|
+
"/usr",
|
|
953
|
+
"/usr/bin",
|
|
954
|
+
"/usr/share",
|
|
955
|
+
"/usr/share/applications",
|
|
956
|
+
] {
|
|
957
|
+
builder.with_dir_entry(FileOptions::dir(directory).permissions(0o755))?;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
builder.with_dir(source, format!("/opt/{rpm_package}"), |options| options)?;
|
|
961
|
+
builder.with_symlink(FileOptions::symlink(
|
|
962
|
+
format!("/usr/bin/{rpm_package}"),
|
|
963
|
+
&executable,
|
|
964
|
+
))?;
|
|
965
|
+
builder.with_file_contents(
|
|
966
|
+
rpm_desktop_file(package, &rpm_package, &executable),
|
|
967
|
+
FileOptions::new(format!("/usr/share/applications/{rpm_package}.desktop"))
|
|
968
|
+
.permissions(0o644),
|
|
969
|
+
)?;
|
|
970
|
+
|
|
971
|
+
let rpm = builder.build()?;
|
|
972
|
+
rpm.write_file(artifact)
|
|
973
|
+
.with_context(|| format!("Could not write {}", artifact.display()))
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
#[derive(Debug)]
|
|
977
|
+
struct MsiPayload {
|
|
978
|
+
directories: Vec<MsiDirectoryEntry>,
|
|
979
|
+
files: Vec<MsiFileEntry>,
|
|
980
|
+
shortcut_component: Option<String>,
|
|
981
|
+
shortcut_target_file: Option<String>,
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
#[derive(Debug)]
|
|
985
|
+
struct MsiDirectoryEntry {
|
|
986
|
+
id: String,
|
|
987
|
+
parent_id: String,
|
|
988
|
+
name: String,
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
#[derive(Debug)]
|
|
992
|
+
struct MsiFileEntry {
|
|
993
|
+
id: String,
|
|
994
|
+
component_id: String,
|
|
995
|
+
component_guid: String,
|
|
996
|
+
directory_id: String,
|
|
997
|
+
source: PathBuf,
|
|
998
|
+
file_name: String,
|
|
999
|
+
cabinet_name: String,
|
|
1000
|
+
size: i32,
|
|
1001
|
+
sequence: i32,
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
fn write_msi_archive(package: &PackageReport, artifact: &Path) -> Result<()> {
|
|
1005
|
+
if package.platform() != "win32" {
|
|
1006
|
+
bail!(
|
|
1007
|
+
"MSI maker only supports Windows packages. Requested {}.",
|
|
1008
|
+
package.platform()
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
let source = Path::new(package.bundle_dir().as_str());
|
|
1013
|
+
if !source.exists() {
|
|
1014
|
+
bail!("Package output does not exist: {}", source.display());
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
let parent = artifact
|
|
1018
|
+
.parent()
|
|
1019
|
+
.with_context(|| format!("Artifact path has no parent: {}", artifact.display()))?;
|
|
1020
|
+
fs::create_dir_all(parent).with_context(|| format!("Could not create {}", parent.display()))?;
|
|
1021
|
+
|
|
1022
|
+
let payload = collect_msi_payload(package, source)?;
|
|
1023
|
+
if payload.files.is_empty() {
|
|
1024
|
+
bail!(
|
|
1025
|
+
"MSI maker requires at least one packaged file in {}",
|
|
1026
|
+
source.display()
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
let cabinet = create_msi_cabinet(&payload)?;
|
|
1031
|
+
let file = fs::OpenOptions::new()
|
|
1032
|
+
.read(true)
|
|
1033
|
+
.write(true)
|
|
1034
|
+
.create(true)
|
|
1035
|
+
.truncate(true)
|
|
1036
|
+
.open(artifact)
|
|
1037
|
+
.with_context(|| format!("Could not create {}", artifact.display()))?;
|
|
1038
|
+
let mut installer =
|
|
1039
|
+
Package::create(PackageType::Installer, file).context("Could not create MSI package")?;
|
|
1040
|
+
|
|
1041
|
+
write_msi_summary(&mut installer, package)?;
|
|
1042
|
+
create_msi_tables(&mut installer)?;
|
|
1043
|
+
insert_msi_rows(&mut installer, package, &payload)?;
|
|
1044
|
+
{
|
|
1045
|
+
let mut stream = installer
|
|
1046
|
+
.write_stream("app.cab")
|
|
1047
|
+
.context("Could not create embedded MSI cabinet stream")?;
|
|
1048
|
+
stream
|
|
1049
|
+
.write_all(&cabinet)
|
|
1050
|
+
.context("Could not write embedded MSI cabinet stream")?;
|
|
1051
|
+
}
|
|
1052
|
+
installer.flush().context("Could not flush MSI package")?;
|
|
1053
|
+
installer
|
|
1054
|
+
.into_inner()
|
|
1055
|
+
.context("Could not finish MSI package")?;
|
|
1056
|
+
|
|
1057
|
+
Ok(())
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
fn write_msi_summary(installer: &mut Package<File>, package: &PackageReport) -> Result<()> {
|
|
1061
|
+
let package_code = deterministic_guid(
|
|
1062
|
+
"package-code",
|
|
1063
|
+
&[
|
|
1064
|
+
package.app_name(),
|
|
1065
|
+
package.project().version.as_deref().unwrap_or("0.1.0"),
|
|
1066
|
+
package.arch(),
|
|
1067
|
+
],
|
|
1068
|
+
);
|
|
1069
|
+
let arch = msi_summary_arch(package.arch());
|
|
1070
|
+
let language = Language::from_code(1033);
|
|
1071
|
+
let summary = installer.summary_info_mut();
|
|
1072
|
+
summary.set_title(format!("{} Installer", package.app_name()));
|
|
1073
|
+
summary.set_subject(package.app_name().to_string());
|
|
1074
|
+
summary.set_author("electron-cli".to_string());
|
|
1075
|
+
summary.set_comments(format!(
|
|
1076
|
+
"{} packaged by electron-cli.",
|
|
1077
|
+
single_line(package.app_name())
|
|
1078
|
+
));
|
|
1079
|
+
summary.set_creating_application("electron-cli".to_string());
|
|
1080
|
+
summary.set_uuid(package_code);
|
|
1081
|
+
summary.set_arch(arch.to_string());
|
|
1082
|
+
summary.set_languages(&[language]);
|
|
1083
|
+
summary.set_page_count(200);
|
|
1084
|
+
summary.set_word_count(2);
|
|
1085
|
+
Ok(())
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
fn create_msi_tables(installer: &mut Package<File>) -> Result<()> {
|
|
1089
|
+
create_msi_table(
|
|
1090
|
+
installer,
|
|
1091
|
+
"Property",
|
|
1092
|
+
vec![
|
|
1093
|
+
Column::build("Property").primary_key().id_string(72),
|
|
1094
|
+
Column::build("Value").nullable().formatted_string(0),
|
|
1095
|
+
],
|
|
1096
|
+
)?;
|
|
1097
|
+
create_msi_table(
|
|
1098
|
+
installer,
|
|
1099
|
+
"Directory",
|
|
1100
|
+
vec![
|
|
1101
|
+
Column::build("Directory").primary_key().id_string(72),
|
|
1102
|
+
Column::build("Directory_Parent").nullable().id_string(72),
|
|
1103
|
+
Column::build("DefaultDir").text_string(255),
|
|
1104
|
+
],
|
|
1105
|
+
)?;
|
|
1106
|
+
create_msi_table(
|
|
1107
|
+
installer,
|
|
1108
|
+
"Feature",
|
|
1109
|
+
vec![
|
|
1110
|
+
Column::build("Feature").primary_key().id_string(38),
|
|
1111
|
+
Column::build("Feature_Parent").nullable().id_string(38),
|
|
1112
|
+
Column::build("Title").nullable().text_string(64),
|
|
1113
|
+
Column::build("Description").nullable().text_string(255),
|
|
1114
|
+
Column::build("Display").nullable().int16(),
|
|
1115
|
+
Column::build("Level").int16(),
|
|
1116
|
+
Column::build("Directory_").nullable().id_string(72),
|
|
1117
|
+
Column::build("Attributes").int16(),
|
|
1118
|
+
],
|
|
1119
|
+
)?;
|
|
1120
|
+
create_msi_table(
|
|
1121
|
+
installer,
|
|
1122
|
+
"Component",
|
|
1123
|
+
vec![
|
|
1124
|
+
Column::build("Component").primary_key().id_string(72),
|
|
1125
|
+
Column::build("ComponentId").nullable().string(38),
|
|
1126
|
+
Column::build("Directory_").id_string(72),
|
|
1127
|
+
Column::build("Attributes").int16(),
|
|
1128
|
+
Column::build("Condition").nullable().formatted_string(255),
|
|
1129
|
+
Column::build("KeyPath").nullable().id_string(72),
|
|
1130
|
+
],
|
|
1131
|
+
)?;
|
|
1132
|
+
create_msi_table(
|
|
1133
|
+
installer,
|
|
1134
|
+
"FeatureComponents",
|
|
1135
|
+
vec![
|
|
1136
|
+
Column::build("Feature_").primary_key().id_string(38),
|
|
1137
|
+
Column::build("Component_").primary_key().id_string(72),
|
|
1138
|
+
],
|
|
1139
|
+
)?;
|
|
1140
|
+
create_msi_table(
|
|
1141
|
+
installer,
|
|
1142
|
+
"File",
|
|
1143
|
+
vec![
|
|
1144
|
+
Column::build("File").primary_key().id_string(72),
|
|
1145
|
+
Column::build("Component_").id_string(72),
|
|
1146
|
+
Column::build("FileName").text_string(255),
|
|
1147
|
+
Column::build("FileSize").int32(),
|
|
1148
|
+
Column::build("Version").nullable().string(72),
|
|
1149
|
+
Column::build("Language").nullable().string(20),
|
|
1150
|
+
Column::build("Attributes").nullable().int16(),
|
|
1151
|
+
Column::build("Sequence").int16(),
|
|
1152
|
+
],
|
|
1153
|
+
)?;
|
|
1154
|
+
create_msi_table(
|
|
1155
|
+
installer,
|
|
1156
|
+
"Media",
|
|
1157
|
+
vec![
|
|
1158
|
+
Column::build("DiskId").primary_key().int16(),
|
|
1159
|
+
Column::build("LastSequence").int16(),
|
|
1160
|
+
Column::build("DiskPrompt").nullable().text_string(64),
|
|
1161
|
+
Column::build("Cabinet").nullable().string(255),
|
|
1162
|
+
Column::build("VolumeLabel").nullable().string(32),
|
|
1163
|
+
Column::build("Source").nullable().string(72),
|
|
1164
|
+
],
|
|
1165
|
+
)?;
|
|
1166
|
+
create_msi_table(
|
|
1167
|
+
installer,
|
|
1168
|
+
"Shortcut",
|
|
1169
|
+
vec![
|
|
1170
|
+
Column::build("Shortcut").primary_key().id_string(72),
|
|
1171
|
+
Column::build("Directory_").id_string(72),
|
|
1172
|
+
Column::build("Name").text_string(128),
|
|
1173
|
+
Column::build("Component_").id_string(72),
|
|
1174
|
+
Column::build("Target").formatted_string(0),
|
|
1175
|
+
Column::build("Arguments").nullable().formatted_string(255),
|
|
1176
|
+
Column::build("Description").nullable().text_string(255),
|
|
1177
|
+
Column::build("Hotkey").nullable().int16(),
|
|
1178
|
+
Column::build("Icon_").nullable().id_string(72),
|
|
1179
|
+
Column::build("IconIndex").nullable().int16(),
|
|
1180
|
+
Column::build("ShowCmd").nullable().int16(),
|
|
1181
|
+
Column::build("WkDir").nullable().id_string(72),
|
|
1182
|
+
],
|
|
1183
|
+
)?;
|
|
1184
|
+
create_msi_table(
|
|
1185
|
+
installer,
|
|
1186
|
+
"RemoveFile",
|
|
1187
|
+
vec![
|
|
1188
|
+
Column::build("FileKey").primary_key().id_string(72),
|
|
1189
|
+
Column::build("Component_").id_string(72),
|
|
1190
|
+
Column::build("FileName").nullable().text_string(255),
|
|
1191
|
+
Column::build("DirProperty").id_string(72),
|
|
1192
|
+
Column::build("InstallMode").int16(),
|
|
1193
|
+
],
|
|
1194
|
+
)?;
|
|
1195
|
+
create_msi_table(
|
|
1196
|
+
installer,
|
|
1197
|
+
"InstallExecuteSequence",
|
|
1198
|
+
vec![
|
|
1199
|
+
Column::build("Action").primary_key().id_string(72),
|
|
1200
|
+
Column::build("Condition").nullable().formatted_string(255),
|
|
1201
|
+
Column::build("Sequence").nullable().int16(),
|
|
1202
|
+
],
|
|
1203
|
+
)?;
|
|
1204
|
+
create_msi_table(
|
|
1205
|
+
installer,
|
|
1206
|
+
"ActionText",
|
|
1207
|
+
vec![
|
|
1208
|
+
Column::build("Action").primary_key().id_string(72),
|
|
1209
|
+
Column::build("Description").nullable().text_string(64),
|
|
1210
|
+
Column::build("Template").nullable().formatted_string(128),
|
|
1211
|
+
],
|
|
1212
|
+
)
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
fn create_msi_table(installer: &mut Package<File>, name: &str, columns: Vec<Column>) -> Result<()> {
|
|
1216
|
+
installer
|
|
1217
|
+
.create_table(name, columns)
|
|
1218
|
+
.with_context(|| format!("Could not create MSI {name} table"))
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
fn insert_msi_rows(
|
|
1222
|
+
installer: &mut Package<File>,
|
|
1223
|
+
package: &PackageReport,
|
|
1224
|
+
payload: &MsiPayload,
|
|
1225
|
+
) -> Result<()> {
|
|
1226
|
+
let product_version = msi_product_version(package.project().version.as_deref());
|
|
1227
|
+
let product_code = msi_guid(deterministic_guid(
|
|
1228
|
+
"product-code",
|
|
1229
|
+
&[package.app_name(), &product_version, package.arch()],
|
|
1230
|
+
));
|
|
1231
|
+
let upgrade_code = msi_guid(deterministic_guid(
|
|
1232
|
+
"upgrade-code",
|
|
1233
|
+
&[
|
|
1234
|
+
package.app_name(),
|
|
1235
|
+
package.project().name.as_deref().unwrap_or(""),
|
|
1236
|
+
],
|
|
1237
|
+
));
|
|
1238
|
+
insert_msi_table_rows(
|
|
1239
|
+
installer,
|
|
1240
|
+
"Property",
|
|
1241
|
+
vec![
|
|
1242
|
+
vec![s("ProductCode"), s(product_code)],
|
|
1243
|
+
vec![s("ProductLanguage"), s("1033")],
|
|
1244
|
+
vec![s("ProductName"), s(package.app_name())],
|
|
1245
|
+
vec![s("ProductVersion"), s(product_version)],
|
|
1246
|
+
vec![s("Manufacturer"), s("electron-cli")],
|
|
1247
|
+
vec![s("UpgradeCode"), s(upgrade_code)],
|
|
1248
|
+
vec![s("ALLUSERS"), s("1")],
|
|
1249
|
+
vec![s("INSTALLLEVEL"), s("1")],
|
|
1250
|
+
],
|
|
1251
|
+
)?;
|
|
1252
|
+
|
|
1253
|
+
let program_files_dir = msi_program_files_directory(package.arch());
|
|
1254
|
+
let install_folder = msi_filename("APPDIR", package.app_name());
|
|
1255
|
+
insert_msi_table_rows(
|
|
1256
|
+
installer,
|
|
1257
|
+
"Directory",
|
|
1258
|
+
vec![
|
|
1259
|
+
vec![s("TARGETDIR"), Value::Null, s("SourceDir")],
|
|
1260
|
+
vec![s(program_files_dir), s("TARGETDIR"), s(".")],
|
|
1261
|
+
vec![s("INSTALLFOLDER"), s(program_files_dir), s(install_folder)],
|
|
1262
|
+
vec![s("ProgramMenuFolder"), s("TARGETDIR"), s(".")],
|
|
1263
|
+
vec![
|
|
1264
|
+
s("ApplicationProgramsFolder"),
|
|
1265
|
+
s("ProgramMenuFolder"),
|
|
1266
|
+
s(msi_filename("APPMENU", package.app_name())),
|
|
1267
|
+
],
|
|
1268
|
+
],
|
|
1269
|
+
)?;
|
|
1270
|
+
insert_msi_table_rows(
|
|
1271
|
+
installer,
|
|
1272
|
+
"Directory",
|
|
1273
|
+
payload
|
|
1274
|
+
.directories
|
|
1275
|
+
.iter()
|
|
1276
|
+
.map(|directory| {
|
|
1277
|
+
vec![
|
|
1278
|
+
s(&directory.id),
|
|
1279
|
+
s(&directory.parent_id),
|
|
1280
|
+
s(&directory.name),
|
|
1281
|
+
]
|
|
1282
|
+
})
|
|
1283
|
+
.collect(),
|
|
1284
|
+
)?;
|
|
1285
|
+
|
|
1286
|
+
insert_msi_table_rows(
|
|
1287
|
+
installer,
|
|
1288
|
+
"Feature",
|
|
1289
|
+
vec![vec![
|
|
1290
|
+
s("MainFeature"),
|
|
1291
|
+
Value::Null,
|
|
1292
|
+
s(package.app_name()),
|
|
1293
|
+
s(format!("Install {}.", single_line(package.app_name()))),
|
|
1294
|
+
Value::from(1),
|
|
1295
|
+
Value::from(1),
|
|
1296
|
+
s("INSTALLFOLDER"),
|
|
1297
|
+
Value::from(0),
|
|
1298
|
+
]],
|
|
1299
|
+
)?;
|
|
1300
|
+
|
|
1301
|
+
let component_attributes = msi_component_attributes(package.arch());
|
|
1302
|
+
insert_msi_table_rows(
|
|
1303
|
+
installer,
|
|
1304
|
+
"Component",
|
|
1305
|
+
payload
|
|
1306
|
+
.files
|
|
1307
|
+
.iter()
|
|
1308
|
+
.map(|file| {
|
|
1309
|
+
vec![
|
|
1310
|
+
s(&file.component_id),
|
|
1311
|
+
s(&file.component_guid),
|
|
1312
|
+
s(&file.directory_id),
|
|
1313
|
+
Value::from(component_attributes),
|
|
1314
|
+
Value::Null,
|
|
1315
|
+
s(&file.id),
|
|
1316
|
+
]
|
|
1317
|
+
})
|
|
1318
|
+
.collect(),
|
|
1319
|
+
)?;
|
|
1320
|
+
insert_msi_table_rows(
|
|
1321
|
+
installer,
|
|
1322
|
+
"FeatureComponents",
|
|
1323
|
+
payload
|
|
1324
|
+
.files
|
|
1325
|
+
.iter()
|
|
1326
|
+
.map(|file| vec![s("MainFeature"), s(&file.component_id)])
|
|
1327
|
+
.collect(),
|
|
1328
|
+
)?;
|
|
1329
|
+
insert_msi_table_rows(
|
|
1330
|
+
installer,
|
|
1331
|
+
"File",
|
|
1332
|
+
payload
|
|
1333
|
+
.files
|
|
1334
|
+
.iter()
|
|
1335
|
+
.map(|file| {
|
|
1336
|
+
vec![
|
|
1337
|
+
s(&file.id),
|
|
1338
|
+
s(&file.component_id),
|
|
1339
|
+
s(&file.file_name),
|
|
1340
|
+
Value::from(file.size),
|
|
1341
|
+
Value::Null,
|
|
1342
|
+
Value::Null,
|
|
1343
|
+
Value::from(0),
|
|
1344
|
+
Value::from(file.sequence),
|
|
1345
|
+
]
|
|
1346
|
+
})
|
|
1347
|
+
.collect(),
|
|
1348
|
+
)?;
|
|
1349
|
+
insert_msi_table_rows(
|
|
1350
|
+
installer,
|
|
1351
|
+
"Media",
|
|
1352
|
+
vec![vec![
|
|
1353
|
+
Value::from(1),
|
|
1354
|
+
Value::from(payload.files.len() as i32),
|
|
1355
|
+
Value::Null,
|
|
1356
|
+
s("#app.cab"),
|
|
1357
|
+
Value::Null,
|
|
1358
|
+
Value::Null,
|
|
1359
|
+
]],
|
|
1360
|
+
)?;
|
|
1361
|
+
|
|
1362
|
+
if let (Some(component), Some(target_file)) =
|
|
1363
|
+
(&payload.shortcut_component, &payload.shortcut_target_file)
|
|
1364
|
+
{
|
|
1365
|
+
insert_msi_table_rows(
|
|
1366
|
+
installer,
|
|
1367
|
+
"Shortcut",
|
|
1368
|
+
vec![vec![
|
|
1369
|
+
s("ApplicationShortcut"),
|
|
1370
|
+
s("ApplicationProgramsFolder"),
|
|
1371
|
+
s(msi_filename("SHORTCUT", package.app_name())),
|
|
1372
|
+
s(component),
|
|
1373
|
+
s(format!("[#{target_file}]")),
|
|
1374
|
+
Value::Null,
|
|
1375
|
+
s(format!("Launch {}.", single_line(package.app_name()))),
|
|
1376
|
+
Value::Null,
|
|
1377
|
+
Value::Null,
|
|
1378
|
+
Value::Null,
|
|
1379
|
+
Value::Null,
|
|
1380
|
+
s("INSTALLFOLDER"),
|
|
1381
|
+
]],
|
|
1382
|
+
)?;
|
|
1383
|
+
insert_msi_table_rows(
|
|
1384
|
+
installer,
|
|
1385
|
+
"RemoveFile",
|
|
1386
|
+
vec![vec![
|
|
1387
|
+
s("RemoveStartMenuFolder"),
|
|
1388
|
+
s(component),
|
|
1389
|
+
Value::Null,
|
|
1390
|
+
s("ApplicationProgramsFolder"),
|
|
1391
|
+
Value::from(2),
|
|
1392
|
+
]],
|
|
1393
|
+
)?;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
insert_msi_table_rows(
|
|
1397
|
+
installer,
|
|
1398
|
+
"InstallExecuteSequence",
|
|
1399
|
+
vec![
|
|
1400
|
+
standard_action("CostInitialize", 800),
|
|
1401
|
+
standard_action("FileCost", 900),
|
|
1402
|
+
standard_action("CostFinalize", 1000),
|
|
1403
|
+
standard_action("InstallValidate", 1400),
|
|
1404
|
+
standard_action("InstallInitialize", 1500),
|
|
1405
|
+
standard_action("ProcessComponents", 1600),
|
|
1406
|
+
standard_action("UnpublishFeatures", 1800),
|
|
1407
|
+
standard_action("RemoveShortcuts", 3200),
|
|
1408
|
+
standard_action("RemoveFiles", 3500),
|
|
1409
|
+
standard_action("InstallFiles", 4000),
|
|
1410
|
+
standard_action("CreateShortcuts", 4500),
|
|
1411
|
+
standard_action("RegisterUser", 6000),
|
|
1412
|
+
standard_action("RegisterProduct", 6100),
|
|
1413
|
+
standard_action("PublishFeatures", 6300),
|
|
1414
|
+
standard_action("PublishProduct", 6400),
|
|
1415
|
+
standard_action("InstallFinalize", 6600),
|
|
1416
|
+
],
|
|
1417
|
+
)?;
|
|
1418
|
+
insert_msi_table_rows(
|
|
1419
|
+
installer,
|
|
1420
|
+
"ActionText",
|
|
1421
|
+
vec![
|
|
1422
|
+
action_text(
|
|
1423
|
+
"InstallFiles",
|
|
1424
|
+
"Copying new files",
|
|
1425
|
+
"File: [1], Directory: [9], Size: [6]",
|
|
1426
|
+
),
|
|
1427
|
+
action_text("CreateShortcuts", "Creating shortcuts", "Shortcut: [1]"),
|
|
1428
|
+
action_text("RemoveFiles", "Removing files", "File: [1], Directory: [9]"),
|
|
1429
|
+
action_text("RemoveShortcuts", "Removing shortcuts", "Shortcut: [1]"),
|
|
1430
|
+
],
|
|
1431
|
+
)
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
fn insert_msi_table_rows(
|
|
1435
|
+
installer: &mut Package<File>,
|
|
1436
|
+
table: &str,
|
|
1437
|
+
rows: Vec<Vec<Value>>,
|
|
1438
|
+
) -> Result<()> {
|
|
1439
|
+
if rows.is_empty() {
|
|
1440
|
+
return Ok(());
|
|
1441
|
+
}
|
|
1442
|
+
installer
|
|
1443
|
+
.insert_rows(Insert::into(table).rows(rows))
|
|
1444
|
+
.with_context(|| format!("Could not insert MSI {table} rows"))
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
fn standard_action(action: &str, sequence: i32) -> Vec<Value> {
|
|
1448
|
+
vec![s(action), Value::Null, Value::from(sequence)]
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
fn action_text(action: &str, description: &str, template: &str) -> Vec<Value> {
|
|
1452
|
+
vec![s(action), s(description), s(template)]
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
fn collect_msi_payload(package: &PackageReport, source: &Path) -> Result<MsiPayload> {
|
|
1456
|
+
let mut payload = MsiPayload {
|
|
1457
|
+
directories: Vec::new(),
|
|
1458
|
+
files: Vec::new(),
|
|
1459
|
+
shortcut_component: None,
|
|
1460
|
+
shortcut_target_file: None,
|
|
1461
|
+
};
|
|
1462
|
+
let mut directory_ids = BTreeMap::from([(PathBuf::new(), "INSTALLFOLDER".to_string())]);
|
|
1463
|
+
collect_msi_directory(
|
|
1464
|
+
package,
|
|
1465
|
+
source,
|
|
1466
|
+
Path::new(""),
|
|
1467
|
+
"INSTALLFOLDER",
|
|
1468
|
+
&mut directory_ids,
|
|
1469
|
+
&mut payload,
|
|
1470
|
+
)?;
|
|
1471
|
+
|
|
1472
|
+
if payload.files.len() > i16::MAX as usize {
|
|
1473
|
+
bail!(
|
|
1474
|
+
"MSI maker supports up to {} files; package contains {}.",
|
|
1475
|
+
i16::MAX,
|
|
1476
|
+
payload.files.len()
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
Ok(payload)
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
fn collect_msi_directory(
|
|
1484
|
+
package: &PackageReport,
|
|
1485
|
+
source: &Path,
|
|
1486
|
+
relative_dir: &Path,
|
|
1487
|
+
directory_id: &str,
|
|
1488
|
+
directory_ids: &mut BTreeMap<PathBuf, String>,
|
|
1489
|
+
payload: &mut MsiPayload,
|
|
1490
|
+
) -> Result<()> {
|
|
1491
|
+
let mut entries = fs::read_dir(source)
|
|
1492
|
+
.with_context(|| format!("Could not read {}", source.display()))?
|
|
1493
|
+
.collect::<Result<Vec<_>, io::Error>>()?;
|
|
1494
|
+
entries.sort_by_key(|entry| entry.path());
|
|
1495
|
+
|
|
1496
|
+
for entry in entries {
|
|
1497
|
+
let path = entry.path();
|
|
1498
|
+
let file_name = utf8_file_name(&path)?.to_string();
|
|
1499
|
+
let relative_path = relative_dir.join(&file_name);
|
|
1500
|
+
let metadata = fs::symlink_metadata(&path)
|
|
1501
|
+
.with_context(|| format!("Could not stat {}", path.display()))?;
|
|
1502
|
+
|
|
1503
|
+
if metadata.file_type().is_symlink() {
|
|
1504
|
+
bail!(
|
|
1505
|
+
"MSI maker does not support symbolic links yet: {}",
|
|
1506
|
+
path.display()
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
if metadata.is_dir() {
|
|
1511
|
+
let dir_id = format!("D{:04}", directory_ids.len());
|
|
1512
|
+
directory_ids.insert(relative_path.clone(), dir_id.clone());
|
|
1513
|
+
payload.directories.push(MsiDirectoryEntry {
|
|
1514
|
+
id: dir_id.clone(),
|
|
1515
|
+
parent_id: directory_id.to_string(),
|
|
1516
|
+
name: msi_filename(&dir_id, &file_name),
|
|
1517
|
+
});
|
|
1518
|
+
collect_msi_directory(
|
|
1519
|
+
package,
|
|
1520
|
+
&path,
|
|
1521
|
+
&relative_path,
|
|
1522
|
+
&dir_id,
|
|
1523
|
+
directory_ids,
|
|
1524
|
+
payload,
|
|
1525
|
+
)?;
|
|
1526
|
+
} else if metadata.is_file() {
|
|
1527
|
+
let sequence = payload.files.len() + 1;
|
|
1528
|
+
let size = i32::try_from(metadata.len())
|
|
1529
|
+
.with_context(|| format!("MSI file is too large: {}", path.display()))?;
|
|
1530
|
+
let file_id = format!("F{sequence:04}");
|
|
1531
|
+
let component_id = format!("C{sequence:04}");
|
|
1532
|
+
let relative_key = relative_path.to_string_lossy().replace('\\', "/");
|
|
1533
|
+
let component_guid = msi_guid(deterministic_guid(
|
|
1534
|
+
"component",
|
|
1535
|
+
&[
|
|
1536
|
+
package.app_name(),
|
|
1537
|
+
package.project().name.as_deref().unwrap_or(""),
|
|
1538
|
+
&relative_key,
|
|
1539
|
+
],
|
|
1540
|
+
));
|
|
1541
|
+
let entry = MsiFileEntry {
|
|
1542
|
+
id: file_id.clone(),
|
|
1543
|
+
component_id: component_id.clone(),
|
|
1544
|
+
component_guid,
|
|
1545
|
+
directory_id: directory_id.to_string(),
|
|
1546
|
+
source: path.clone(),
|
|
1547
|
+
file_name: msi_filename(&file_id, &file_name),
|
|
1548
|
+
cabinet_name: file_id.clone(),
|
|
1549
|
+
size,
|
|
1550
|
+
sequence: sequence as i32,
|
|
1551
|
+
};
|
|
1552
|
+
|
|
1553
|
+
if file_name.eq_ignore_ascii_case(package.executable_name()) {
|
|
1554
|
+
payload.shortcut_component = Some(component_id);
|
|
1555
|
+
payload.shortcut_target_file = Some(file_id);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
payload.files.push(entry);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
Ok(())
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
fn create_msi_cabinet(payload: &MsiPayload) -> Result<Vec<u8>> {
|
|
1566
|
+
let mut builder = CabinetBuilder::new();
|
|
1567
|
+
{
|
|
1568
|
+
let folder = builder.add_folder(CabCompressionType::MsZip);
|
|
1569
|
+
for file in &payload.files {
|
|
1570
|
+
folder.add_file(&file.cabinet_name);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
let cursor = Cursor::new(Vec::new());
|
|
1575
|
+
let mut cabinet = builder
|
|
1576
|
+
.build(cursor)
|
|
1577
|
+
.context("Could not start MSI cabinet")?;
|
|
1578
|
+
for file in &payload.files {
|
|
1579
|
+
let mut writer = cabinet
|
|
1580
|
+
.next_file()
|
|
1581
|
+
.context("Could not open next MSI cabinet file")?
|
|
1582
|
+
.context("MSI cabinet writer finished before all files were written")?;
|
|
1583
|
+
anyhow::ensure!(
|
|
1584
|
+
writer.file_name() == file.cabinet_name,
|
|
1585
|
+
"MSI cabinet file order drifted while writing {}",
|
|
1586
|
+
file.source.display()
|
|
1587
|
+
);
|
|
1588
|
+
let mut source = File::open(&file.source)
|
|
1589
|
+
.with_context(|| format!("Could not open {}", file.source.display()))?;
|
|
1590
|
+
io::copy(&mut source, &mut writer)
|
|
1591
|
+
.with_context(|| format!("Could not add {} to MSI cabinet", file.source.display()))?;
|
|
1592
|
+
}
|
|
1593
|
+
let cursor = cabinet.finish().context("Could not finish MSI cabinet")?;
|
|
1594
|
+
Ok(cursor.into_inner())
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
fn debian_control_file(
|
|
1598
|
+
package: &PackageReport,
|
|
1599
|
+
deb_package: &str,
|
|
1600
|
+
version: &str,
|
|
1601
|
+
arch: &str,
|
|
1602
|
+
installed_size: u64,
|
|
1603
|
+
) -> String {
|
|
1604
|
+
format!(
|
|
1605
|
+
"Package: {deb_package}\n\
|
|
1606
|
+
Version: {version}\n\
|
|
1607
|
+
Section: utils\n\
|
|
1608
|
+
Priority: optional\n\
|
|
1609
|
+
Architecture: {arch}\n\
|
|
1610
|
+
Maintainer: electron-cli <noreply@example.invalid>\n\
|
|
1611
|
+
Installed-Size: {installed_size}\n\
|
|
1612
|
+
Description: {description}\n\
|
|
1613
|
+
Electron application packaged by electron-cli.\n",
|
|
1614
|
+
description = single_line(package.app_name())
|
|
1615
|
+
)
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
fn append_deb_data_tar(
|
|
1619
|
+
builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
|
|
1620
|
+
package: &PackageReport,
|
|
1621
|
+
source: &Path,
|
|
1622
|
+
deb_package: &str,
|
|
1623
|
+
) -> Result<()> {
|
|
1624
|
+
for directory in [
|
|
1625
|
+
"./",
|
|
1626
|
+
"./opt",
|
|
1627
|
+
"./usr",
|
|
1628
|
+
"./usr/bin",
|
|
1629
|
+
"./usr/share",
|
|
1630
|
+
"./usr/share/applications",
|
|
1631
|
+
] {
|
|
1632
|
+
append_directory_to_tar(builder, directory, 0o755)?;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
let app_root = format!("./opt/{deb_package}");
|
|
1636
|
+
append_directory_to_tar(builder, &app_root, 0o755)?;
|
|
1637
|
+
append_directory_contents_to_tar(builder, source, Path::new(&app_root))?;
|
|
1638
|
+
|
|
1639
|
+
let executable = format!("/opt/{deb_package}/{}", package.executable_name());
|
|
1640
|
+
append_symlink_to_tar(
|
|
1641
|
+
builder,
|
|
1642
|
+
format!("./usr/bin/{deb_package}"),
|
|
1643
|
+
&executable,
|
|
1644
|
+
0o777,
|
|
1645
|
+
)?;
|
|
1646
|
+
append_bytes_to_tar(
|
|
1647
|
+
builder,
|
|
1648
|
+
format!("./usr/share/applications/{deb_package}.desktop"),
|
|
1649
|
+
debian_desktop_file(package, deb_package, &executable).as_bytes(),
|
|
1650
|
+
0o644,
|
|
1651
|
+
)?;
|
|
1652
|
+
|
|
1653
|
+
Ok(())
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
fn debian_desktop_file(package: &PackageReport, deb_package: &str, executable: &str) -> String {
|
|
1657
|
+
desktop_file(package, deb_package, executable)
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
fn rpm_desktop_file(package: &PackageReport, rpm_package: &str, executable: &str) -> String {
|
|
1661
|
+
desktop_file(package, rpm_package, executable)
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
fn desktop_file(package: &PackageReport, package_name: &str, executable: &str) -> String {
|
|
1665
|
+
format!(
|
|
1666
|
+
"[Desktop Entry]\n\
|
|
1667
|
+
Name={name}\n\
|
|
1668
|
+
Exec={executable} %U\n\
|
|
1669
|
+
Terminal=false\n\
|
|
1670
|
+
Type=Application\n\
|
|
1671
|
+
StartupWMClass={wm_class}\n\
|
|
1672
|
+
Categories=Utility;\n",
|
|
1673
|
+
name = single_line(package.app_name()),
|
|
1674
|
+
wm_class = package_name
|
|
1675
|
+
)
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
fn gzip_tar(
|
|
1679
|
+
write_contents: impl FnOnce(&mut TarBuilder<GzEncoder<Vec<u8>>>) -> Result<()>,
|
|
1680
|
+
) -> Result<Vec<u8>> {
|
|
1681
|
+
let encoder = GzEncoder::new(Vec::new(), Compression::default());
|
|
1682
|
+
let mut builder = TarBuilder::new(encoder);
|
|
1683
|
+
builder.mode(tar::HeaderMode::Deterministic);
|
|
1684
|
+
write_contents(&mut builder)?;
|
|
1685
|
+
builder.finish().context("Could not finish tar archive")?;
|
|
1686
|
+
let encoder = builder
|
|
1687
|
+
.into_inner()
|
|
1688
|
+
.context("Could not retrieve gzip encoder")?;
|
|
1689
|
+
encoder.finish().context("Could not finish gzip archive")
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
fn append_directory_contents_to_tar(
|
|
1693
|
+
builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
|
|
1694
|
+
source: &Path,
|
|
1695
|
+
destination: &Path,
|
|
1696
|
+
) -> Result<()> {
|
|
1697
|
+
let mut entries = fs::read_dir(source)
|
|
1698
|
+
.with_context(|| format!("Could not read {}", source.display()))?
|
|
1699
|
+
.collect::<Result<Vec<_>, io::Error>>()?;
|
|
1700
|
+
entries.sort_by_key(|entry| entry.path());
|
|
1701
|
+
|
|
1702
|
+
for entry in entries {
|
|
1703
|
+
let source_path = entry.path();
|
|
1704
|
+
let destination_path = destination.join(entry.file_name());
|
|
1705
|
+
append_path_to_tar(builder, &source_path, &destination_path)?;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
Ok(())
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
fn append_path_to_tar(
|
|
1712
|
+
builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
|
|
1713
|
+
source: &Path,
|
|
1714
|
+
destination: &Path,
|
|
1715
|
+
) -> Result<()> {
|
|
1716
|
+
let metadata = fs::symlink_metadata(source)
|
|
1717
|
+
.with_context(|| format!("Could not stat {}", source.display()))?;
|
|
1718
|
+
|
|
1719
|
+
if metadata.is_dir() {
|
|
1720
|
+
append_directory_to_tar(builder, destination, unix_mode(&metadata, 0o755))?;
|
|
1721
|
+
append_directory_contents_to_tar(builder, source, destination)?;
|
|
1722
|
+
} else if metadata.file_type().is_symlink() {
|
|
1723
|
+
let target = fs::read_link(source)
|
|
1724
|
+
.with_context(|| format!("Could not read link {}", source.display()))?;
|
|
1725
|
+
append_symlink_to_tar(builder, destination, &target, 0o777)?;
|
|
1726
|
+
} else if metadata.is_file() {
|
|
1727
|
+
append_file_to_tar(builder, source, destination, &metadata)?;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
Ok(())
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
fn append_directory_to_tar(
|
|
1734
|
+
builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
|
|
1735
|
+
path: impl AsRef<Path>,
|
|
1736
|
+
mode: u32,
|
|
1737
|
+
) -> Result<()> {
|
|
1738
|
+
let mut header = TarHeader::new_gnu();
|
|
1739
|
+
header.set_entry_type(tar::EntryType::Directory);
|
|
1740
|
+
header.set_size(0);
|
|
1741
|
+
header.set_mode(mode);
|
|
1742
|
+
header.set_mtime(0);
|
|
1743
|
+
header.set_cksum();
|
|
1744
|
+
builder
|
|
1745
|
+
.append_data(&mut header, path.as_ref(), io::empty())
|
|
1746
|
+
.with_context(|| format!("Could not add {} to data tar", path.as_ref().display()))
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
fn append_file_to_tar(
|
|
1750
|
+
builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
|
|
1751
|
+
source: &Path,
|
|
1752
|
+
destination: &Path,
|
|
1753
|
+
metadata: &fs::Metadata,
|
|
1754
|
+
) -> Result<()> {
|
|
1755
|
+
let mut header = TarHeader::new_gnu();
|
|
1756
|
+
header.set_entry_type(tar::EntryType::Regular);
|
|
1757
|
+
header.set_size(metadata.len());
|
|
1758
|
+
header.set_mode(unix_mode(metadata, 0o644));
|
|
1759
|
+
header.set_mtime(0);
|
|
1760
|
+
header.set_cksum();
|
|
1761
|
+
let mut file =
|
|
1762
|
+
File::open(source).with_context(|| format!("Could not open {}", source.display()))?;
|
|
1763
|
+
builder
|
|
1764
|
+
.append_data(&mut header, destination, &mut file)
|
|
1765
|
+
.with_context(|| format!("Could not add {} to data tar", source.display()))
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
fn append_bytes_to_tar(
|
|
1769
|
+
builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
|
|
1770
|
+
path: impl AsRef<Path>,
|
|
1771
|
+
contents: &[u8],
|
|
1772
|
+
mode: u32,
|
|
1773
|
+
) -> Result<()> {
|
|
1774
|
+
let mut header = TarHeader::new_gnu();
|
|
1775
|
+
header.set_entry_type(tar::EntryType::Regular);
|
|
1776
|
+
header.set_size(contents.len() as u64);
|
|
1777
|
+
header.set_mode(mode);
|
|
1778
|
+
header.set_mtime(0);
|
|
1779
|
+
header.set_cksum();
|
|
1780
|
+
builder
|
|
1781
|
+
.append_data(&mut header, path.as_ref(), contents)
|
|
1782
|
+
.with_context(|| format!("Could not add {} to tar", path.as_ref().display()))
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
fn append_symlink_to_tar(
|
|
1786
|
+
builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
|
|
1787
|
+
path: impl AsRef<Path>,
|
|
1788
|
+
target: impl AsRef<Path>,
|
|
1789
|
+
mode: u32,
|
|
1790
|
+
) -> Result<()> {
|
|
1791
|
+
let mut header = TarHeader::new_gnu();
|
|
1792
|
+
header.set_entry_type(tar::EntryType::Symlink);
|
|
1793
|
+
header.set_size(0);
|
|
1794
|
+
header.set_mode(mode);
|
|
1795
|
+
header.set_mtime(0);
|
|
1796
|
+
header
|
|
1797
|
+
.set_link_name(target.as_ref())
|
|
1798
|
+
.with_context(|| format!("Could not set link target for {}", path.as_ref().display()))?;
|
|
1799
|
+
header.set_cksum();
|
|
1800
|
+
builder
|
|
1801
|
+
.append_data(&mut header, path.as_ref(), io::empty())
|
|
1802
|
+
.with_context(|| format!("Could not add {} to tar", path.as_ref().display()))
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
struct ArMember {
|
|
1806
|
+
name: &'static str,
|
|
1807
|
+
mode: u32,
|
|
1808
|
+
data: Vec<u8>,
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
fn write_ar_archive(artifact: &Path, members: &[ArMember]) -> Result<()> {
|
|
1812
|
+
let mut file = BufWriter::new(
|
|
1813
|
+
File::create(artifact)
|
|
1814
|
+
.with_context(|| format!("Could not create {}", artifact.display()))?,
|
|
1815
|
+
);
|
|
1816
|
+
file.write_all(b"!<arch>\n")
|
|
1817
|
+
.with_context(|| format!("Could not write {}", artifact.display()))?;
|
|
1818
|
+
|
|
1819
|
+
for member in members {
|
|
1820
|
+
write_ar_member(&mut file, member)
|
|
1821
|
+
.with_context(|| format!("Could not add {} to {}", member.name, artifact.display()))?;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
file.flush()
|
|
1825
|
+
.with_context(|| format!("Could not finish {}", artifact.display()))
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
fn write_ar_member(writer: &mut impl Write, member: &ArMember) -> Result<()> {
|
|
1829
|
+
let name = format!("{}/", member.name);
|
|
1830
|
+
if name.len() > 16 {
|
|
1831
|
+
bail!("ar member name is too long: {}", member.name);
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
let header = format!(
|
|
1835
|
+
"{name:<16}{mtime:<12}{uid:<6}{gid:<6}{mode:<8o}{size:<10}`\n",
|
|
1836
|
+
mtime = 0,
|
|
1837
|
+
uid = 0,
|
|
1838
|
+
gid = 0,
|
|
1839
|
+
mode = member.mode,
|
|
1840
|
+
size = member.data.len()
|
|
1841
|
+
);
|
|
1842
|
+
debug_assert_eq!(header.len(), 60);
|
|
1843
|
+
writer.write_all(header.as_bytes())?;
|
|
1844
|
+
writer.write_all(&member.data)?;
|
|
1845
|
+
if member.data.len() % 2 == 1 {
|
|
1846
|
+
writer.write_all(b"\n")?;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
Ok(())
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
fn directory_size(path: &Path) -> Result<u64> {
|
|
1853
|
+
let metadata =
|
|
1854
|
+
fs::symlink_metadata(path).with_context(|| format!("Could not stat {}", path.display()))?;
|
|
1855
|
+
if metadata.is_file() {
|
|
1856
|
+
return Ok(metadata.len());
|
|
1857
|
+
}
|
|
1858
|
+
if !metadata.is_dir() {
|
|
1859
|
+
return Ok(0);
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
let mut size = 0;
|
|
1863
|
+
for entry in fs::read_dir(path).with_context(|| format!("Could not read {}", path.display()))? {
|
|
1864
|
+
let entry = entry?;
|
|
1865
|
+
size += directory_size(&entry.path())?;
|
|
1866
|
+
}
|
|
1867
|
+
Ok(size)
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
fn debian_package_name(name: &str) -> String {
|
|
1871
|
+
package_name(name)
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
fn rpm_package_name(name: &str) -> String {
|
|
1875
|
+
package_name(name)
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
fn package_name(name: &str) -> String {
|
|
1879
|
+
let mut package = name
|
|
1880
|
+
.to_ascii_lowercase()
|
|
1881
|
+
.chars()
|
|
1882
|
+
.map(|char| {
|
|
1883
|
+
if char.is_ascii_alphanumeric() || matches!(char, '+' | '-' | '.') {
|
|
1884
|
+
char
|
|
1885
|
+
} else {
|
|
1886
|
+
'-'
|
|
1887
|
+
}
|
|
1888
|
+
})
|
|
1889
|
+
.collect::<String>()
|
|
1890
|
+
.trim_matches(['+', '-', '.'])
|
|
1891
|
+
.to_string();
|
|
1892
|
+
|
|
1893
|
+
if package.len() < 2 {
|
|
1894
|
+
package.push_str("app");
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
package
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
fn debian_version(version: Option<&str>) -> String {
|
|
1901
|
+
let version = version.unwrap_or("0.1.0");
|
|
1902
|
+
let sanitized = version
|
|
1903
|
+
.chars()
|
|
1904
|
+
.map(|char| {
|
|
1905
|
+
if char.is_ascii_alphanumeric() || matches!(char, '.' | '+' | '-' | ':' | '~') {
|
|
1906
|
+
char
|
|
1907
|
+
} else {
|
|
1908
|
+
'~'
|
|
1909
|
+
}
|
|
1910
|
+
})
|
|
1911
|
+
.collect::<String>()
|
|
1912
|
+
.trim_matches(['-', '~'])
|
|
1913
|
+
.to_string();
|
|
1914
|
+
|
|
1915
|
+
if sanitized.is_empty() {
|
|
1916
|
+
"0.1.0".to_string()
|
|
1917
|
+
} else {
|
|
1918
|
+
sanitized
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
fn dmg_version(version: Option<&str>) -> String {
|
|
1923
|
+
let version = version.unwrap_or("0.1.0");
|
|
1924
|
+
let sanitized = version
|
|
1925
|
+
.chars()
|
|
1926
|
+
.map(|char| {
|
|
1927
|
+
if char.is_ascii_alphanumeric() || matches!(char, '.' | '-' | '_') {
|
|
1928
|
+
char
|
|
1929
|
+
} else {
|
|
1930
|
+
'-'
|
|
1931
|
+
}
|
|
1932
|
+
})
|
|
1933
|
+
.collect::<String>()
|
|
1934
|
+
.trim_matches(['-', '.', '_'])
|
|
1935
|
+
.to_string();
|
|
1936
|
+
|
|
1937
|
+
if sanitized.is_empty() {
|
|
1938
|
+
"0.1.0".to_string()
|
|
1939
|
+
} else {
|
|
1940
|
+
sanitized
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
fn rpm_version(version: Option<&str>) -> String {
|
|
1945
|
+
let version = version.unwrap_or("0.1.0");
|
|
1946
|
+
let sanitized = version
|
|
1947
|
+
.chars()
|
|
1948
|
+
.map(|char| {
|
|
1949
|
+
if char.is_ascii_alphanumeric() || matches!(char, '.' | '+' | '_' | '~') {
|
|
1950
|
+
char
|
|
1951
|
+
} else {
|
|
1952
|
+
'_'
|
|
1953
|
+
}
|
|
1954
|
+
})
|
|
1955
|
+
.collect::<String>()
|
|
1956
|
+
.trim_matches(['_', '~'])
|
|
1957
|
+
.to_string();
|
|
1958
|
+
|
|
1959
|
+
if sanitized.is_empty() {
|
|
1960
|
+
"0.1.0".to_string()
|
|
1961
|
+
} else {
|
|
1962
|
+
sanitized
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
fn windows_artifact_version(version: Option<&str>) -> String {
|
|
1967
|
+
let version = version.unwrap_or("0.1.0");
|
|
1968
|
+
let sanitized = version
|
|
1969
|
+
.chars()
|
|
1970
|
+
.map(|char| {
|
|
1971
|
+
if char.is_ascii_alphanumeric() || matches!(char, '.' | '-' | '_') {
|
|
1972
|
+
char
|
|
1973
|
+
} else {
|
|
1974
|
+
'-'
|
|
1975
|
+
}
|
|
1976
|
+
})
|
|
1977
|
+
.collect::<String>()
|
|
1978
|
+
.trim_matches(['-', '.', '_'])
|
|
1979
|
+
.to_string();
|
|
1980
|
+
|
|
1981
|
+
if sanitized.is_empty() {
|
|
1982
|
+
"0.1.0".to_string()
|
|
1983
|
+
} else {
|
|
1984
|
+
sanitized
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
fn msi_product_version(version: Option<&str>) -> String {
|
|
1989
|
+
let mut numbers = version
|
|
1990
|
+
.unwrap_or("0.1.0")
|
|
1991
|
+
.split(|char: char| !char.is_ascii_digit())
|
|
1992
|
+
.filter(|part| !part.is_empty())
|
|
1993
|
+
.filter_map(|part| part.parse::<u32>().ok())
|
|
1994
|
+
.take(3)
|
|
1995
|
+
.collect::<Vec<_>>();
|
|
1996
|
+
while numbers.len() < 3 {
|
|
1997
|
+
numbers.push(0);
|
|
1998
|
+
}
|
|
1999
|
+
if numbers.iter().all(|number| *number == 0) {
|
|
2000
|
+
numbers = vec![0, 1, 0];
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
format!(
|
|
2004
|
+
"{}.{}.{}",
|
|
2005
|
+
numbers[0].min(255),
|
|
2006
|
+
numbers[1].min(255),
|
|
2007
|
+
numbers[2].min(65_535)
|
|
2008
|
+
)
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
fn debian_arch(arch: &str) -> String {
|
|
2012
|
+
match arch {
|
|
2013
|
+
"x64" => "amd64".to_string(),
|
|
2014
|
+
"ia32" => "i386".to_string(),
|
|
2015
|
+
"armv7l" => "armhf".to_string(),
|
|
2016
|
+
arch => arch.to_string(),
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
fn rpm_arch(arch: &str) -> String {
|
|
2021
|
+
match arch {
|
|
2022
|
+
"x64" => "x86_64".to_string(),
|
|
2023
|
+
"arm64" => "aarch64".to_string(),
|
|
2024
|
+
"ia32" => "i386".to_string(),
|
|
2025
|
+
"armv7l" => "armv7hl".to_string(),
|
|
2026
|
+
arch => arch.to_string(),
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
fn windows_arch(arch: &str) -> String {
|
|
2031
|
+
match arch {
|
|
2032
|
+
"ia32" => "x86".to_string(),
|
|
2033
|
+
arch => arch.to_string(),
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
fn msi_summary_arch(arch: &str) -> &'static str {
|
|
2038
|
+
match arch {
|
|
2039
|
+
"x64" => "x64",
|
|
2040
|
+
"arm64" => "Arm64",
|
|
2041
|
+
_ => "Intel",
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
fn msi_program_files_directory(arch: &str) -> &'static str {
|
|
2046
|
+
match arch {
|
|
2047
|
+
"x64" | "arm64" => "ProgramFiles64Folder",
|
|
2048
|
+
_ => "ProgramFilesFolder",
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
fn msi_component_attributes(arch: &str) -> i32 {
|
|
2053
|
+
match arch {
|
|
2054
|
+
"x64" | "arm64" => 256,
|
|
2055
|
+
_ => 0,
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
fn msi_filename(id: &str, long_name: &str) -> String {
|
|
2060
|
+
if is_msi_short_name(long_name) {
|
|
2061
|
+
return long_name.to_string();
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
let extension = Path::new(long_name)
|
|
2065
|
+
.extension()
|
|
2066
|
+
.and_then(|extension| extension.to_str())
|
|
2067
|
+
.map(|extension| {
|
|
2068
|
+
extension
|
|
2069
|
+
.chars()
|
|
2070
|
+
.filter(|char| char.is_ascii_alphanumeric())
|
|
2071
|
+
.take(3)
|
|
2072
|
+
.collect::<String>()
|
|
2073
|
+
})
|
|
2074
|
+
.filter(|extension| !extension.is_empty());
|
|
2075
|
+
let stem = id
|
|
2076
|
+
.chars()
|
|
2077
|
+
.filter(|char| char.is_ascii_alphanumeric())
|
|
2078
|
+
.take(8)
|
|
2079
|
+
.collect::<String>();
|
|
2080
|
+
let short = match extension {
|
|
2081
|
+
Some(extension) => format!("{stem}.{extension}"),
|
|
2082
|
+
None => stem,
|
|
2083
|
+
};
|
|
2084
|
+
format!("{short}|{long_name}")
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
fn is_msi_short_name(name: &str) -> bool {
|
|
2088
|
+
let Some(file_name) = Path::new(name).file_name().and_then(|name| name.to_str()) else {
|
|
2089
|
+
return false;
|
|
2090
|
+
};
|
|
2091
|
+
if file_name != name || file_name.is_empty() || file_name.contains(' ') {
|
|
2092
|
+
return false;
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
let mut parts = file_name.split('.');
|
|
2096
|
+
let stem = parts.next().unwrap_or_default();
|
|
2097
|
+
let extension = parts.next();
|
|
2098
|
+
if parts.next().is_some() || stem.is_empty() || stem.len() > 8 {
|
|
2099
|
+
return false;
|
|
2100
|
+
}
|
|
2101
|
+
if extension.is_some_and(|extension| extension.is_empty() || extension.len() > 3) {
|
|
2102
|
+
return false;
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
file_name
|
|
2106
|
+
.chars()
|
|
2107
|
+
.all(|char| char.is_ascii_alphanumeric() || matches!(char, '_' | '$' | '~' | '!' | '#'))
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
fn deterministic_guid(kind: &str, parts: &[&str]) -> Uuid {
|
|
2111
|
+
let key = format!("electron-cli:{kind}:{}", parts.join(":"));
|
|
2112
|
+
Uuid::new_v5(&Uuid::NAMESPACE_URL, key.as_bytes())
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
fn msi_guid(uuid: Uuid) -> String {
|
|
2116
|
+
format!("{{{}}}", uuid.hyphenated()).to_ascii_uppercase()
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
fn s(value: impl Into<String>) -> Value {
|
|
2120
|
+
Value::from(value.into())
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
fn single_line(value: &str) -> String {
|
|
2124
|
+
value
|
|
2125
|
+
.chars()
|
|
2126
|
+
.map(|char| {
|
|
2127
|
+
if char == '\n' || char == '\r' {
|
|
2128
|
+
' '
|
|
2129
|
+
} else {
|
|
2130
|
+
char
|
|
2131
|
+
}
|
|
2132
|
+
})
|
|
2133
|
+
.collect()
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
#[cfg(unix)]
|
|
2137
|
+
fn unix_mode(metadata: &fs::Metadata, _fallback: u32) -> u32 {
|
|
2138
|
+
use std::os::unix::fs::PermissionsExt;
|
|
2139
|
+
|
|
2140
|
+
metadata.permissions().mode()
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
#[cfg(not(unix))]
|
|
2144
|
+
fn unix_mode(_metadata: &fs::Metadata, fallback: u32) -> u32 {
|
|
2145
|
+
fallback
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
fn utf8_path(path: PathBuf) -> Result<Utf8PathBuf> {
|
|
2149
|
+
Utf8PathBuf::from_path_buf(path).map_err(|path| {
|
|
2150
|
+
anyhow::anyhow!(
|
|
2151
|
+
"Path contains invalid UTF-8 and cannot be represented in JSON: {}",
|
|
2152
|
+
path.display()
|
|
2153
|
+
)
|
|
2154
|
+
})
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
impl MakeStatus {
|
|
2158
|
+
fn as_str(&self) -> &'static str {
|
|
2159
|
+
match self {
|
|
2160
|
+
MakeStatus::Planned => "planned",
|
|
2161
|
+
MakeStatus::Made => "made",
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
impl MakeReport {
|
|
2167
|
+
pub(crate) fn mark_made(&mut self) -> Result<()> {
|
|
2168
|
+
self.status = MakeStatus::Made;
|
|
2169
|
+
self.artifact_size = Some(
|
|
2170
|
+
fs::metadata(self.artifact.as_str())
|
|
2171
|
+
.with_context(|| format!("Could not stat {}", self.artifact))?
|
|
2172
|
+
.len(),
|
|
2173
|
+
);
|
|
2174
|
+
Ok(())
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
pub(crate) fn package(&self) -> &PackageReport {
|
|
2178
|
+
&self.package
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
pub(crate) fn target(&self) -> &str {
|
|
2182
|
+
&self.target
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
pub(crate) fn artifact(&self) -> &Utf8PathBuf {
|
|
2186
|
+
&self.artifact
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
pub(crate) fn warnings(&self) -> &[String] {
|
|
2190
|
+
&self.warnings
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
#[cfg(test)]
|
|
2195
|
+
mod tests {
|
|
2196
|
+
use super::*;
|
|
2197
|
+
use std::io::Read;
|
|
2198
|
+
use zip::ZipArchive;
|
|
2199
|
+
|
|
2200
|
+
#[test]
|
|
2201
|
+
fn builds_make_report_for_zip_target() {
|
|
2202
|
+
let root = unique_temp_dir("plan");
|
|
2203
|
+
write_package_json(&root);
|
|
2204
|
+
write_app_file(&root);
|
|
2205
|
+
write_fake_electron_dist(&root);
|
|
2206
|
+
|
|
2207
|
+
let args = MakeArgs {
|
|
2208
|
+
cwd: root.clone(),
|
|
2209
|
+
out_dir: PathBuf::from("out"),
|
|
2210
|
+
name: None,
|
|
2211
|
+
platform: None,
|
|
2212
|
+
arch: None,
|
|
2213
|
+
target: Some(crate::cli::MakeTarget::Zip),
|
|
2214
|
+
skip_package: false,
|
|
2215
|
+
force: false,
|
|
2216
|
+
dry_run: true,
|
|
2217
|
+
json: true,
|
|
2218
|
+
};
|
|
2219
|
+
let report = build_report(&args).expect("report should build");
|
|
2220
|
+
|
|
2221
|
+
assert_eq!(report.target, "zip");
|
|
2222
|
+
let expected_suffix = PathBuf::from("out")
|
|
2223
|
+
.join("make")
|
|
2224
|
+
.join("zip")
|
|
2225
|
+
.join(report.package.platform())
|
|
2226
|
+
.join(report.package.arch())
|
|
2227
|
+
.join(format!(
|
|
2228
|
+
"starter-app-{}-{}.zip",
|
|
2229
|
+
report.package.platform(),
|
|
2230
|
+
report.package.arch()
|
|
2231
|
+
));
|
|
2232
|
+
assert!(Path::new(report.artifact.as_str()).ends_with(expected_suffix));
|
|
2233
|
+
|
|
2234
|
+
let _ = fs::remove_dir_all(root);
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
#[test]
|
|
2238
|
+
fn builds_make_report_for_deb_target() {
|
|
2239
|
+
let root = unique_temp_dir("deb-plan");
|
|
2240
|
+
write_package_json(&root);
|
|
2241
|
+
write_app_file(&root);
|
|
2242
|
+
write_fake_electron_dist(&root);
|
|
2243
|
+
|
|
2244
|
+
let args = MakeArgs {
|
|
2245
|
+
cwd: root.clone(),
|
|
2246
|
+
out_dir: PathBuf::from("out"),
|
|
2247
|
+
name: None,
|
|
2248
|
+
platform: Some("linux".to_string()),
|
|
2249
|
+
arch: Some("x64".to_string()),
|
|
2250
|
+
target: Some(crate::cli::MakeTarget::Deb),
|
|
2251
|
+
skip_package: false,
|
|
2252
|
+
force: false,
|
|
2253
|
+
dry_run: true,
|
|
2254
|
+
json: true,
|
|
2255
|
+
};
|
|
2256
|
+
let report = build_report(&args).expect("report should build");
|
|
2257
|
+
|
|
2258
|
+
assert_eq!(report.target, "deb");
|
|
2259
|
+
assert!(Path::new(report.artifact.as_str()).ends_with(
|
|
2260
|
+
PathBuf::from("out")
|
|
2261
|
+
.join("make")
|
|
2262
|
+
.join("deb")
|
|
2263
|
+
.join("linux")
|
|
2264
|
+
.join("x64")
|
|
2265
|
+
.join("starter-app_0.1.0_amd64.deb")
|
|
2266
|
+
));
|
|
2267
|
+
|
|
2268
|
+
let _ = fs::remove_dir_all(root);
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
#[test]
|
|
2272
|
+
fn builds_make_report_for_dmg_target() {
|
|
2273
|
+
let root = unique_temp_dir("dmg-plan");
|
|
2274
|
+
write_package_json(&root);
|
|
2275
|
+
write_app_file(&root);
|
|
2276
|
+
write_fake_electron_dist(&root);
|
|
2277
|
+
|
|
2278
|
+
let args = MakeArgs {
|
|
2279
|
+
cwd: root.clone(),
|
|
2280
|
+
out_dir: PathBuf::from("out"),
|
|
2281
|
+
name: None,
|
|
2282
|
+
platform: Some("darwin".to_string()),
|
|
2283
|
+
arch: Some("arm64".to_string()),
|
|
2284
|
+
target: Some(crate::cli::MakeTarget::Dmg),
|
|
2285
|
+
skip_package: false,
|
|
2286
|
+
force: false,
|
|
2287
|
+
dry_run: true,
|
|
2288
|
+
json: true,
|
|
2289
|
+
};
|
|
2290
|
+
let report = build_report(&args).expect("report should build");
|
|
2291
|
+
|
|
2292
|
+
assert_eq!(report.target, "dmg");
|
|
2293
|
+
assert!(Path::new(report.artifact.as_str()).ends_with(
|
|
2294
|
+
PathBuf::from("out")
|
|
2295
|
+
.join("make")
|
|
2296
|
+
.join("dmg")
|
|
2297
|
+
.join("darwin")
|
|
2298
|
+
.join("arm64")
|
|
2299
|
+
.join("starter-app-0.1.0-arm64.dmg")
|
|
2300
|
+
));
|
|
2301
|
+
|
|
2302
|
+
let _ = fs::remove_dir_all(root);
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
#[test]
|
|
2306
|
+
fn builds_make_report_for_rpm_target() {
|
|
2307
|
+
let root = unique_temp_dir("rpm-plan");
|
|
2308
|
+
write_package_json(&root);
|
|
2309
|
+
write_app_file(&root);
|
|
2310
|
+
write_fake_electron_dist(&root);
|
|
2311
|
+
|
|
2312
|
+
let args = MakeArgs {
|
|
2313
|
+
cwd: root.clone(),
|
|
2314
|
+
out_dir: PathBuf::from("out"),
|
|
2315
|
+
name: None,
|
|
2316
|
+
platform: Some("linux".to_string()),
|
|
2317
|
+
arch: Some("x64".to_string()),
|
|
2318
|
+
target: Some(crate::cli::MakeTarget::Rpm),
|
|
2319
|
+
skip_package: false,
|
|
2320
|
+
force: false,
|
|
2321
|
+
dry_run: true,
|
|
2322
|
+
json: true,
|
|
2323
|
+
};
|
|
2324
|
+
let report = build_report(&args).expect("report should build");
|
|
2325
|
+
|
|
2326
|
+
assert_eq!(report.target, "rpm");
|
|
2327
|
+
assert!(Path::new(report.artifact.as_str()).ends_with(
|
|
2328
|
+
PathBuf::from("out")
|
|
2329
|
+
.join("make")
|
|
2330
|
+
.join("rpm")
|
|
2331
|
+
.join("linux")
|
|
2332
|
+
.join("x64")
|
|
2333
|
+
.join("starter-app-0.1.0-1.x86_64.rpm")
|
|
2334
|
+
));
|
|
2335
|
+
|
|
2336
|
+
let _ = fs::remove_dir_all(root);
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
#[test]
|
|
2340
|
+
fn builds_make_report_for_msi_target() {
|
|
2341
|
+
let root = unique_temp_dir("msi-plan");
|
|
2342
|
+
write_package_json(&root);
|
|
2343
|
+
write_app_file(&root);
|
|
2344
|
+
write_fake_electron_dist(&root);
|
|
2345
|
+
|
|
2346
|
+
let args = MakeArgs {
|
|
2347
|
+
cwd: root.clone(),
|
|
2348
|
+
out_dir: PathBuf::from("out"),
|
|
2349
|
+
name: None,
|
|
2350
|
+
platform: Some("win32".to_string()),
|
|
2351
|
+
arch: Some("x64".to_string()),
|
|
2352
|
+
target: Some(crate::cli::MakeTarget::Msi),
|
|
2353
|
+
skip_package: false,
|
|
2354
|
+
force: false,
|
|
2355
|
+
dry_run: true,
|
|
2356
|
+
json: true,
|
|
2357
|
+
};
|
|
2358
|
+
let report = build_report(&args).expect("report should build");
|
|
2359
|
+
|
|
2360
|
+
assert_eq!(report.target, "msi");
|
|
2361
|
+
assert!(Path::new(report.artifact.as_str()).ends_with(
|
|
2362
|
+
PathBuf::from("out")
|
|
2363
|
+
.join("make")
|
|
2364
|
+
.join("msi")
|
|
2365
|
+
.join("win32")
|
|
2366
|
+
.join("x64")
|
|
2367
|
+
.join("starter-app-0.1.0-x64.msi")
|
|
2368
|
+
));
|
|
2369
|
+
|
|
2370
|
+
let _ = fs::remove_dir_all(root);
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
#[test]
|
|
2374
|
+
fn builds_make_reports_from_configured_forge_makers() {
|
|
2375
|
+
let root = unique_temp_dir("configured-makers");
|
|
2376
|
+
write_package_json_with_makers(
|
|
2377
|
+
&root,
|
|
2378
|
+
r#"[
|
|
2379
|
+
{"name":"@electron-forge/maker-zip"},
|
|
2380
|
+
{"name":"@electron-forge/maker-deb","platforms":["linux"]},
|
|
2381
|
+
{"name":"@electron-forge/maker-rpm","platforms":["darwin"]},
|
|
2382
|
+
{"name":"@electron-forge/maker-squirrel","platforms":["linux"]}
|
|
2383
|
+
]"#,
|
|
2384
|
+
);
|
|
2385
|
+
write_app_file(&root);
|
|
2386
|
+
write_fake_electron_dist(&root);
|
|
2387
|
+
|
|
2388
|
+
let args = MakeArgs {
|
|
2389
|
+
cwd: root.clone(),
|
|
2390
|
+
out_dir: PathBuf::from("out"),
|
|
2391
|
+
name: None,
|
|
2392
|
+
platform: Some("linux".to_string()),
|
|
2393
|
+
arch: Some("x64".to_string()),
|
|
2394
|
+
target: None,
|
|
2395
|
+
skip_package: false,
|
|
2396
|
+
force: false,
|
|
2397
|
+
dry_run: true,
|
|
2398
|
+
json: true,
|
|
2399
|
+
};
|
|
2400
|
+
let reports = build_reports(&args).expect("reports should build");
|
|
2401
|
+
|
|
2402
|
+
assert_eq!(reports.len(), 2);
|
|
2403
|
+
assert_eq!(reports[0].target(), "zip");
|
|
2404
|
+
assert_eq!(reports[1].target(), "deb");
|
|
2405
|
+
assert!(reports[0]
|
|
2406
|
+
.warnings()
|
|
2407
|
+
.iter()
|
|
2408
|
+
.any(|warning| warning.contains("@electron-forge/maker-squirrel")));
|
|
2409
|
+
|
|
2410
|
+
let _ = fs::remove_dir_all(root);
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
#[test]
|
|
2414
|
+
fn builds_make_reports_from_static_forge_config_js() {
|
|
2415
|
+
let root = unique_temp_dir("configured-makers-js");
|
|
2416
|
+
write_package_json(&root);
|
|
2417
|
+
fs::write(
|
|
2418
|
+
root.join("forge.config.js"),
|
|
2419
|
+
r#"
|
|
2420
|
+
module.exports = {
|
|
2421
|
+
makers: [
|
|
2422
|
+
{ name: '@electron-forge/maker-zip' },
|
|
2423
|
+
{ name: '@electron-forge/maker-rpm', platforms: ['linux'] },
|
|
2424
|
+
],
|
|
2425
|
+
};
|
|
2426
|
+
"#,
|
|
2427
|
+
)
|
|
2428
|
+
.expect("forge config should be written");
|
|
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("linux".to_string()),
|
|
2437
|
+
arch: Some("x64".to_string()),
|
|
2438
|
+
target: None,
|
|
2439
|
+
skip_package: false,
|
|
2440
|
+
force: false,
|
|
2441
|
+
dry_run: true,
|
|
2442
|
+
json: true,
|
|
2443
|
+
};
|
|
2444
|
+
let reports = build_reports(&args).expect("reports should build");
|
|
2445
|
+
|
|
2446
|
+
assert_eq!(reports.len(), 2);
|
|
2447
|
+
assert_eq!(reports[0].target(), "zip");
|
|
2448
|
+
assert_eq!(reports[1].target(), "rpm");
|
|
2449
|
+
|
|
2450
|
+
let _ = fs::remove_dir_all(root);
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
#[test]
|
|
2454
|
+
fn explicit_make_target_overrides_configured_makers() {
|
|
2455
|
+
let root = unique_temp_dir("target-override");
|
|
2456
|
+
write_package_json_with_makers(
|
|
2457
|
+
&root,
|
|
2458
|
+
r#"[{"name":"@electron-forge/maker-zip"},{"name":"@electron-forge/maker-deb"}]"#,
|
|
2459
|
+
);
|
|
2460
|
+
write_app_file(&root);
|
|
2461
|
+
write_fake_electron_dist(&root);
|
|
2462
|
+
|
|
2463
|
+
let args = MakeArgs {
|
|
2464
|
+
cwd: root.clone(),
|
|
2465
|
+
out_dir: PathBuf::from("out"),
|
|
2466
|
+
name: None,
|
|
2467
|
+
platform: Some("win32".to_string()),
|
|
2468
|
+
arch: Some("x64".to_string()),
|
|
2469
|
+
target: Some(crate::cli::MakeTarget::Msi),
|
|
2470
|
+
skip_package: false,
|
|
2471
|
+
force: false,
|
|
2472
|
+
dry_run: true,
|
|
2473
|
+
json: true,
|
|
2474
|
+
};
|
|
2475
|
+
let report = build_report(&args).expect("report should build");
|
|
2476
|
+
|
|
2477
|
+
assert_eq!(report.target(), "msi");
|
|
2478
|
+
assert!(report
|
|
2479
|
+
.warnings()
|
|
2480
|
+
.iter()
|
|
2481
|
+
.all(|warning| !warning.contains("maker-deb")));
|
|
2482
|
+
|
|
2483
|
+
let _ = fs::remove_dir_all(root);
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
#[test]
|
|
2487
|
+
fn makes_zip_artifact_after_packaging() {
|
|
2488
|
+
let root = unique_temp_dir("execute");
|
|
2489
|
+
write_package_json(&root);
|
|
2490
|
+
write_app_file(&root);
|
|
2491
|
+
write_fake_electron_dist(&root);
|
|
2492
|
+
|
|
2493
|
+
let args = MakeArgs {
|
|
2494
|
+
cwd: root.clone(),
|
|
2495
|
+
out_dir: PathBuf::from("out"),
|
|
2496
|
+
name: None,
|
|
2497
|
+
platform: None,
|
|
2498
|
+
arch: None,
|
|
2499
|
+
target: Some(crate::cli::MakeTarget::Zip),
|
|
2500
|
+
skip_package: false,
|
|
2501
|
+
force: false,
|
|
2502
|
+
dry_run: false,
|
|
2503
|
+
json: false,
|
|
2504
|
+
};
|
|
2505
|
+
let mut report = build_report(&args).expect("report should build");
|
|
2506
|
+
|
|
2507
|
+
execute_make(&mut report, &args).expect("make should succeed");
|
|
2508
|
+
|
|
2509
|
+
let file = File::open(report.artifact.as_str()).expect("artifact should exist");
|
|
2510
|
+
let mut archive = ZipArchive::new(file).expect("zip should open");
|
|
2511
|
+
let app_entry = if report.package.platform() == "darwin" {
|
|
2512
|
+
"starter-app.app/Contents/Resources/app/package.json".to_string()
|
|
2513
|
+
} else {
|
|
2514
|
+
format!(
|
|
2515
|
+
"starter-app-{}-{}/resources/app/package.json",
|
|
2516
|
+
report.package.platform(),
|
|
2517
|
+
report.package.arch()
|
|
2518
|
+
)
|
|
2519
|
+
};
|
|
2520
|
+
|
|
2521
|
+
archive
|
|
2522
|
+
.by_name(&app_entry)
|
|
2523
|
+
.expect("app package.json should be archived");
|
|
2524
|
+
|
|
2525
|
+
let _ = fs::remove_dir_all(root);
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
#[test]
|
|
2529
|
+
fn makes_multiple_configured_artifacts_from_existing_package() {
|
|
2530
|
+
let root = unique_temp_dir("configured-execute");
|
|
2531
|
+
write_package_json_with_makers(
|
|
2532
|
+
&root,
|
|
2533
|
+
r#"[
|
|
2534
|
+
{"name":"@electron-forge/maker-zip","platforms":["win32"]},
|
|
2535
|
+
{"name":"@electron-forge/maker-wix","platforms":["win32"]}
|
|
2536
|
+
]"#,
|
|
2537
|
+
);
|
|
2538
|
+
write_app_file(&root);
|
|
2539
|
+
write_fake_windows_bundle(&root.join("out/starter-app-win32-x64"), "starter-app.exe");
|
|
2540
|
+
|
|
2541
|
+
let args = MakeArgs {
|
|
2542
|
+
cwd: root.clone(),
|
|
2543
|
+
out_dir: PathBuf::from("out"),
|
|
2544
|
+
name: None,
|
|
2545
|
+
platform: Some("win32".to_string()),
|
|
2546
|
+
arch: Some("x64".to_string()),
|
|
2547
|
+
target: None,
|
|
2548
|
+
skip_package: true,
|
|
2549
|
+
force: false,
|
|
2550
|
+
dry_run: false,
|
|
2551
|
+
json: true,
|
|
2552
|
+
};
|
|
2553
|
+
let mut reports = build_reports(&args).expect("reports should build");
|
|
2554
|
+
|
|
2555
|
+
execute_make_reports(&mut reports, &args).expect("configured makers should execute");
|
|
2556
|
+
|
|
2557
|
+
assert_eq!(reports.len(), 2);
|
|
2558
|
+
assert_eq!(reports[0].target(), "zip");
|
|
2559
|
+
assert_eq!(reports[1].target(), "msi");
|
|
2560
|
+
assert!(Path::new(reports[0].artifact.as_str()).exists());
|
|
2561
|
+
assert!(Path::new(reports[1].artifact.as_str()).exists());
|
|
2562
|
+
|
|
2563
|
+
let _ = fs::remove_dir_all(root);
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
#[test]
|
|
2567
|
+
fn writes_deb_archive_with_control_and_data_members() {
|
|
2568
|
+
let root = unique_temp_dir("deb-archive");
|
|
2569
|
+
write_package_json(&root);
|
|
2570
|
+
write_app_file(&root);
|
|
2571
|
+
write_fake_electron_dist(&root);
|
|
2572
|
+
|
|
2573
|
+
let args = MakeArgs {
|
|
2574
|
+
cwd: root.clone(),
|
|
2575
|
+
out_dir: PathBuf::from("out"),
|
|
2576
|
+
name: None,
|
|
2577
|
+
platform: Some("linux".to_string()),
|
|
2578
|
+
arch: Some("x64".to_string()),
|
|
2579
|
+
target: Some(crate::cli::MakeTarget::Deb),
|
|
2580
|
+
skip_package: false,
|
|
2581
|
+
force: false,
|
|
2582
|
+
dry_run: true,
|
|
2583
|
+
json: true,
|
|
2584
|
+
};
|
|
2585
|
+
let report = build_report(&args).expect("report should build");
|
|
2586
|
+
let bundle_dir = Path::new(report.package.bundle_dir().as_str());
|
|
2587
|
+
fs::create_dir_all(bundle_dir.join("resources/app"))
|
|
2588
|
+
.expect("fake bundle resources should be created");
|
|
2589
|
+
fs::write(bundle_dir.join("starter-app"), "").expect("fake binary should be written");
|
|
2590
|
+
fs::write(bundle_dir.join("resources/app/package.json"), "{}")
|
|
2591
|
+
.expect("fake app package should be written");
|
|
2592
|
+
|
|
2593
|
+
write_deb_archive(&report.package, Path::new(report.artifact.as_str()))
|
|
2594
|
+
.expect("deb should be written");
|
|
2595
|
+
|
|
2596
|
+
let members = read_ar_members(Path::new(report.artifact.as_str()));
|
|
2597
|
+
assert_eq!(
|
|
2598
|
+
members.get("debian-binary").map(Vec::as_slice),
|
|
2599
|
+
Some(&b"2.0\n"[..])
|
|
2600
|
+
);
|
|
2601
|
+
|
|
2602
|
+
let control = read_tar_file(
|
|
2603
|
+
members
|
|
2604
|
+
.get("control.tar.gz")
|
|
2605
|
+
.expect("control tar should exist"),
|
|
2606
|
+
"control",
|
|
2607
|
+
);
|
|
2608
|
+
assert!(control.contains("Package: starter-app"));
|
|
2609
|
+
assert!(control.contains("Architecture: amd64"));
|
|
2610
|
+
|
|
2611
|
+
let data = members.get("data.tar.gz").expect("data tar should exist");
|
|
2612
|
+
assert!(tar_contains(
|
|
2613
|
+
data,
|
|
2614
|
+
"opt/starter-app/resources/app/package.json"
|
|
2615
|
+
));
|
|
2616
|
+
assert!(tar_contains(
|
|
2617
|
+
data,
|
|
2618
|
+
"usr/share/applications/starter-app.desktop"
|
|
2619
|
+
));
|
|
2620
|
+
assert!(tar_contains(data, "usr/bin/starter-app"));
|
|
2621
|
+
|
|
2622
|
+
let _ = fs::remove_dir_all(root);
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
#[test]
|
|
2626
|
+
fn writes_dmg_archive_with_app_bundle_and_applications_entry() {
|
|
2627
|
+
let root = unique_temp_dir("dmg-archive");
|
|
2628
|
+
write_package_json(&root);
|
|
2629
|
+
write_app_file(&root);
|
|
2630
|
+
write_fake_electron_dist(&root);
|
|
2631
|
+
|
|
2632
|
+
let args = MakeArgs {
|
|
2633
|
+
cwd: root.clone(),
|
|
2634
|
+
out_dir: PathBuf::from("out"),
|
|
2635
|
+
name: None,
|
|
2636
|
+
platform: Some("darwin".to_string()),
|
|
2637
|
+
arch: Some("arm64".to_string()),
|
|
2638
|
+
target: Some(crate::cli::MakeTarget::Dmg),
|
|
2639
|
+
skip_package: false,
|
|
2640
|
+
force: false,
|
|
2641
|
+
dry_run: true,
|
|
2642
|
+
json: true,
|
|
2643
|
+
};
|
|
2644
|
+
let report = build_report(&args).expect("report should build");
|
|
2645
|
+
write_fake_macos_bundle(
|
|
2646
|
+
Path::new(report.package.bundle_dir().as_str()),
|
|
2647
|
+
"starter-app",
|
|
2648
|
+
);
|
|
2649
|
+
|
|
2650
|
+
write_dmg_archive(&report.package, Path::new(report.artifact.as_str()))
|
|
2651
|
+
.expect("dmg should be written");
|
|
2652
|
+
|
|
2653
|
+
let mut dmg = apple_dmg::DmgReader::open(Path::new(report.artifact.as_str()))
|
|
2654
|
+
.expect("dmg should parse");
|
|
2655
|
+
assert_eq!(dmg.plist().partitions().len(), 2);
|
|
2656
|
+
let fat32 = dmg.partition_data(1).expect("fat32 partition should read");
|
|
2657
|
+
let fs = fatfs::FileSystem::new(Cursor::new(fat32), fatfs::FsOptions::new())
|
|
2658
|
+
.expect("fat32 should mount");
|
|
2659
|
+
let root_dir = fs.root_dir();
|
|
2660
|
+
let entries = root_dir
|
|
2661
|
+
.iter()
|
|
2662
|
+
.map(|entry| entry.expect("fat entry should read").file_name())
|
|
2663
|
+
.collect::<Vec<_>>();
|
|
2664
|
+
assert!(entries.contains(&"starter-app.app".to_string()));
|
|
2665
|
+
assert!(entries.contains(&"Applications".to_string()));
|
|
2666
|
+
|
|
2667
|
+
let app_dir = root_dir
|
|
2668
|
+
.open_dir("starter-app.app")
|
|
2669
|
+
.expect("app bundle should exist");
|
|
2670
|
+
let contents = app_dir.open_dir("Contents").expect("Contents should exist");
|
|
2671
|
+
let resources = contents
|
|
2672
|
+
.open_dir("Resources")
|
|
2673
|
+
.expect("Resources should exist");
|
|
2674
|
+
let app_resources = resources
|
|
2675
|
+
.open_dir("app")
|
|
2676
|
+
.expect("app resources should exist");
|
|
2677
|
+
app_resources
|
|
2678
|
+
.open_file("package.json")
|
|
2679
|
+
.expect("app package should exist");
|
|
2680
|
+
|
|
2681
|
+
let mut applications = String::new();
|
|
2682
|
+
root_dir
|
|
2683
|
+
.open_file("Applications")
|
|
2684
|
+
.expect("Applications entry should exist")
|
|
2685
|
+
.read_to_string(&mut applications)
|
|
2686
|
+
.expect("Applications entry should read");
|
|
2687
|
+
assert!(applications.starts_with("XSym\n0013\n"));
|
|
2688
|
+
assert!(applications.contains("/Applications"));
|
|
2689
|
+
|
|
2690
|
+
let _ = fs::remove_dir_all(root);
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
#[test]
|
|
2694
|
+
fn writes_rpm_archive_with_metadata_and_payload_entries() {
|
|
2695
|
+
let root = unique_temp_dir("rpm-archive");
|
|
2696
|
+
write_package_json(&root);
|
|
2697
|
+
write_app_file(&root);
|
|
2698
|
+
write_fake_electron_dist(&root);
|
|
2699
|
+
|
|
2700
|
+
let args = MakeArgs {
|
|
2701
|
+
cwd: root.clone(),
|
|
2702
|
+
out_dir: PathBuf::from("out"),
|
|
2703
|
+
name: None,
|
|
2704
|
+
platform: Some("linux".to_string()),
|
|
2705
|
+
arch: Some("x64".to_string()),
|
|
2706
|
+
target: Some(crate::cli::MakeTarget::Rpm),
|
|
2707
|
+
skip_package: false,
|
|
2708
|
+
force: false,
|
|
2709
|
+
dry_run: true,
|
|
2710
|
+
json: true,
|
|
2711
|
+
};
|
|
2712
|
+
let report = build_report(&args).expect("report should build");
|
|
2713
|
+
let bundle_dir = Path::new(report.package.bundle_dir().as_str());
|
|
2714
|
+
fs::create_dir_all(bundle_dir.join("resources/app"))
|
|
2715
|
+
.expect("fake bundle resources should be created");
|
|
2716
|
+
fs::write(bundle_dir.join("starter-app"), "").expect("fake binary should be written");
|
|
2717
|
+
fs::write(bundle_dir.join("resources/app/package.json"), "{}")
|
|
2718
|
+
.expect("fake app package should be written");
|
|
2719
|
+
|
|
2720
|
+
write_rpm_archive(&report.package, Path::new(report.artifact.as_str()))
|
|
2721
|
+
.expect("rpm should be written");
|
|
2722
|
+
|
|
2723
|
+
let rpm = rpm::Package::open(report.artifact.as_str()).expect("rpm should parse");
|
|
2724
|
+
assert_eq!(
|
|
2725
|
+
rpm.metadata.get_name().expect("name should read"),
|
|
2726
|
+
"starter-app"
|
|
2727
|
+
);
|
|
2728
|
+
assert_eq!(
|
|
2729
|
+
rpm.metadata.get_version().expect("version should read"),
|
|
2730
|
+
"0.1.0"
|
|
2731
|
+
);
|
|
2732
|
+
assert_eq!(
|
|
2733
|
+
rpm.metadata.get_release().expect("release should read"),
|
|
2734
|
+
"1"
|
|
2735
|
+
);
|
|
2736
|
+
assert_eq!(rpm.metadata.get_arch().expect("arch should read"), "x86_64");
|
|
2737
|
+
|
|
2738
|
+
let paths = rpm
|
|
2739
|
+
.metadata
|
|
2740
|
+
.get_file_paths()
|
|
2741
|
+
.expect("file paths should read")
|
|
2742
|
+
.into_iter()
|
|
2743
|
+
.map(|path| path.to_string_lossy().to_string())
|
|
2744
|
+
.collect::<Vec<_>>();
|
|
2745
|
+
assert!(paths.contains(&"/opt/starter-app/resources/app/package.json".to_string()));
|
|
2746
|
+
assert!(paths.contains(&"/usr/share/applications/starter-app.desktop".to_string()));
|
|
2747
|
+
assert!(paths.contains(&"/usr/bin/starter-app".to_string()));
|
|
2748
|
+
|
|
2749
|
+
let _ = fs::remove_dir_all(root);
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
#[test]
|
|
2753
|
+
fn writes_msi_archive_with_database_tables_and_embedded_cabinet() {
|
|
2754
|
+
let root = unique_temp_dir("msi-archive");
|
|
2755
|
+
write_package_json(&root);
|
|
2756
|
+
write_app_file(&root);
|
|
2757
|
+
write_fake_electron_dist(&root);
|
|
2758
|
+
|
|
2759
|
+
let args = MakeArgs {
|
|
2760
|
+
cwd: root.clone(),
|
|
2761
|
+
out_dir: PathBuf::from("out"),
|
|
2762
|
+
name: None,
|
|
2763
|
+
platform: Some("win32".to_string()),
|
|
2764
|
+
arch: Some("x64".to_string()),
|
|
2765
|
+
target: Some(crate::cli::MakeTarget::Msi),
|
|
2766
|
+
skip_package: false,
|
|
2767
|
+
force: false,
|
|
2768
|
+
dry_run: true,
|
|
2769
|
+
json: true,
|
|
2770
|
+
};
|
|
2771
|
+
let report = build_report(&args).expect("report should build");
|
|
2772
|
+
write_fake_windows_bundle(
|
|
2773
|
+
Path::new(report.package.bundle_dir().as_str()),
|
|
2774
|
+
"starter-app.exe",
|
|
2775
|
+
);
|
|
2776
|
+
|
|
2777
|
+
write_msi_archive(&report.package, Path::new(report.artifact.as_str()))
|
|
2778
|
+
.expect("msi should be written");
|
|
2779
|
+
|
|
2780
|
+
let mut installer = msi::open(report.artifact.as_str()).expect("msi should parse");
|
|
2781
|
+
assert_eq!(installer.summary_info().arch(), Some("x64"));
|
|
2782
|
+
assert!(installer.has_table("Property"));
|
|
2783
|
+
assert!(installer.has_table("Directory"));
|
|
2784
|
+
assert!(installer.has_table("File"));
|
|
2785
|
+
assert!(installer.has_table("Media"));
|
|
2786
|
+
assert!(installer.has_stream("app.cab"));
|
|
2787
|
+
|
|
2788
|
+
let properties = msi_rows(&mut installer, "Property");
|
|
2789
|
+
assert!(properties.contains(&vec![
|
|
2790
|
+
Value::from("ProductName"),
|
|
2791
|
+
Value::from("starter-app")
|
|
2792
|
+
]));
|
|
2793
|
+
assert!(properties.contains(&vec![Value::from("ProductVersion"), Value::from("0.1.0")]));
|
|
2794
|
+
|
|
2795
|
+
let files = msi_rows(&mut installer, "File");
|
|
2796
|
+
assert!(files
|
|
2797
|
+
.iter()
|
|
2798
|
+
.any(|row| row[2] == Value::from("F0001.jso|package.json")));
|
|
2799
|
+
assert!(files
|
|
2800
|
+
.iter()
|
|
2801
|
+
.any(|row| row[2] == Value::from("F0002.exe|starter-app.exe")));
|
|
2802
|
+
|
|
2803
|
+
let mut cabinet_bytes = Vec::new();
|
|
2804
|
+
installer
|
|
2805
|
+
.read_stream("app.cab")
|
|
2806
|
+
.expect("cab stream should open")
|
|
2807
|
+
.read_to_end(&mut cabinet_bytes)
|
|
2808
|
+
.expect("cab stream should read");
|
|
2809
|
+
let mut cabinet =
|
|
2810
|
+
cab::Cabinet::new(Cursor::new(cabinet_bytes)).expect("cabinet should parse");
|
|
2811
|
+
let mut package_json = String::new();
|
|
2812
|
+
cabinet
|
|
2813
|
+
.read_file("F0001")
|
|
2814
|
+
.expect("package.json cabinet entry should open")
|
|
2815
|
+
.read_to_string(&mut package_json)
|
|
2816
|
+
.expect("package.json cabinet entry should read");
|
|
2817
|
+
assert_eq!(package_json, "{}");
|
|
2818
|
+
|
|
2819
|
+
let _ = fs::remove_dir_all(root);
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
#[test]
|
|
2823
|
+
fn makes_deb_artifact_after_packaging_on_linux() {
|
|
2824
|
+
if !cfg!(target_os = "linux") {
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
let root = unique_temp_dir("deb-execute");
|
|
2829
|
+
write_package_json(&root);
|
|
2830
|
+
write_app_file(&root);
|
|
2831
|
+
write_fake_electron_dist(&root);
|
|
2832
|
+
|
|
2833
|
+
let args = MakeArgs {
|
|
2834
|
+
cwd: root.clone(),
|
|
2835
|
+
out_dir: PathBuf::from("out"),
|
|
2836
|
+
name: None,
|
|
2837
|
+
platform: None,
|
|
2838
|
+
arch: None,
|
|
2839
|
+
target: Some(crate::cli::MakeTarget::Deb),
|
|
2840
|
+
skip_package: false,
|
|
2841
|
+
force: false,
|
|
2842
|
+
dry_run: false,
|
|
2843
|
+
json: false,
|
|
2844
|
+
};
|
|
2845
|
+
let mut report = build_report(&args).expect("report should build");
|
|
2846
|
+
|
|
2847
|
+
execute_make(&mut report, &args).expect("make should succeed");
|
|
2848
|
+
|
|
2849
|
+
assert!(Path::new(report.artifact.as_str()).exists());
|
|
2850
|
+
|
|
2851
|
+
let _ = fs::remove_dir_all(root);
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
#[test]
|
|
2855
|
+
fn makes_dmg_artifact_after_packaging_on_macos() {
|
|
2856
|
+
if !cfg!(target_os = "macos") {
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
let root = unique_temp_dir("dmg-execute");
|
|
2861
|
+
write_package_json(&root);
|
|
2862
|
+
write_app_file(&root);
|
|
2863
|
+
write_fake_electron_dist(&root);
|
|
2864
|
+
|
|
2865
|
+
let args = MakeArgs {
|
|
2866
|
+
cwd: root.clone(),
|
|
2867
|
+
out_dir: PathBuf::from("out"),
|
|
2868
|
+
name: None,
|
|
2869
|
+
platform: None,
|
|
2870
|
+
arch: None,
|
|
2871
|
+
target: Some(crate::cli::MakeTarget::Dmg),
|
|
2872
|
+
skip_package: false,
|
|
2873
|
+
force: false,
|
|
2874
|
+
dry_run: false,
|
|
2875
|
+
json: false,
|
|
2876
|
+
};
|
|
2877
|
+
let mut report = build_report(&args).expect("report should build");
|
|
2878
|
+
|
|
2879
|
+
execute_make(&mut report, &args).expect("make should succeed");
|
|
2880
|
+
|
|
2881
|
+
assert!(Path::new(report.artifact.as_str()).exists());
|
|
2882
|
+
|
|
2883
|
+
let _ = fs::remove_dir_all(root);
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
#[test]
|
|
2887
|
+
fn makes_rpm_artifact_after_packaging_on_linux() {
|
|
2888
|
+
if !cfg!(target_os = "linux") {
|
|
2889
|
+
return;
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
let root = unique_temp_dir("rpm-execute");
|
|
2893
|
+
write_package_json(&root);
|
|
2894
|
+
write_app_file(&root);
|
|
2895
|
+
write_fake_electron_dist(&root);
|
|
2896
|
+
|
|
2897
|
+
let args = MakeArgs {
|
|
2898
|
+
cwd: root.clone(),
|
|
2899
|
+
out_dir: PathBuf::from("out"),
|
|
2900
|
+
name: None,
|
|
2901
|
+
platform: None,
|
|
2902
|
+
arch: None,
|
|
2903
|
+
target: Some(crate::cli::MakeTarget::Rpm),
|
|
2904
|
+
skip_package: false,
|
|
2905
|
+
force: false,
|
|
2906
|
+
dry_run: false,
|
|
2907
|
+
json: false,
|
|
2908
|
+
};
|
|
2909
|
+
let mut report = build_report(&args).expect("report should build");
|
|
2910
|
+
|
|
2911
|
+
execute_make(&mut report, &args).expect("make should succeed");
|
|
2912
|
+
|
|
2913
|
+
assert!(Path::new(report.artifact.as_str()).exists());
|
|
2914
|
+
|
|
2915
|
+
let _ = fs::remove_dir_all(root);
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
fn write_package_json(root: &Path) {
|
|
2919
|
+
fs::write(
|
|
2920
|
+
root.join("package.json"),
|
|
2921
|
+
r#"{"name":"starter-app","version":"0.1.0","license":"MIT","main":"src/main.js","devDependencies":{"electron":"30.0.0"}}"#,
|
|
2922
|
+
)
|
|
2923
|
+
.expect("package.json should be written");
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
fn write_package_json_with_makers(root: &Path, makers: &str) {
|
|
2927
|
+
fs::write(
|
|
2928
|
+
root.join("package.json"),
|
|
2929
|
+
format!(
|
|
2930
|
+
r#"{{
|
|
2931
|
+
"name":"starter-app",
|
|
2932
|
+
"version":"0.1.0",
|
|
2933
|
+
"license":"MIT",
|
|
2934
|
+
"main":"src/main.js",
|
|
2935
|
+
"devDependencies":{{"electron":"30.0.0"}},
|
|
2936
|
+
"config":{{"forge":{{"makers":{makers}}}}}
|
|
2937
|
+
}}"#
|
|
2938
|
+
),
|
|
2939
|
+
)
|
|
2940
|
+
.expect("package.json with makers should be written");
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
fn write_app_file(root: &Path) {
|
|
2944
|
+
fs::create_dir_all(root.join("src")).expect("src should be created");
|
|
2945
|
+
fs::write(root.join("src/main.js"), "console.log('hello');")
|
|
2946
|
+
.expect("main file should be written");
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
fn write_fake_macos_bundle(bundle_dir: &Path, executable_name: &str) {
|
|
2950
|
+
fs::create_dir_all(bundle_dir.join("Contents/MacOS"))
|
|
2951
|
+
.expect("fake macOS executable directory should be created");
|
|
2952
|
+
fs::create_dir_all(bundle_dir.join("Contents/Resources/app"))
|
|
2953
|
+
.expect("fake macOS resources should be created");
|
|
2954
|
+
fs::write(
|
|
2955
|
+
bundle_dir.join("Contents/MacOS").join(executable_name),
|
|
2956
|
+
"#!/bin/sh\n",
|
|
2957
|
+
)
|
|
2958
|
+
.expect("fake macOS executable should be written");
|
|
2959
|
+
fs::write(bundle_dir.join("Contents/Info.plist"), "<plist/>")
|
|
2960
|
+
.expect("fake macOS plist should be written");
|
|
2961
|
+
fs::write(bundle_dir.join("Contents/Resources/app/package.json"), "{}")
|
|
2962
|
+
.expect("fake app package should be written");
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
fn write_fake_windows_bundle(bundle_dir: &Path, executable_name: &str) {
|
|
2966
|
+
fs::create_dir_all(bundle_dir.join("resources/app"))
|
|
2967
|
+
.expect("fake Windows resources should be created");
|
|
2968
|
+
fs::write(bundle_dir.join(executable_name), "fake exe")
|
|
2969
|
+
.expect("fake Windows executable should be written");
|
|
2970
|
+
fs::write(bundle_dir.join("resources/app/package.json"), "{}")
|
|
2971
|
+
.expect("fake app package should be written");
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
fn write_fake_electron_dist(root: &Path) {
|
|
2975
|
+
let dist = root.join("node_modules/electron/dist");
|
|
2976
|
+
if cfg!(target_os = "macos") {
|
|
2977
|
+
let app = dist.join("Electron.app/Contents/MacOS");
|
|
2978
|
+
fs::create_dir_all(&app).expect("fake macOS electron app should be created");
|
|
2979
|
+
fs::write(app.join("Electron"), "").expect("fake macOS binary should be written");
|
|
2980
|
+
} else if cfg!(target_os = "windows") {
|
|
2981
|
+
fs::create_dir_all(&dist).expect("fake electron dist should be created");
|
|
2982
|
+
fs::write(dist.join("electron.exe"), "").expect("fake exe should be written");
|
|
2983
|
+
} else {
|
|
2984
|
+
fs::create_dir_all(&dist).expect("fake electron dist should be created");
|
|
2985
|
+
fs::write(dist.join("electron"), "").expect("fake binary should be written");
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
fn unique_temp_dir(label: &str) -> PathBuf {
|
|
2990
|
+
let nanos = std::time::SystemTime::now()
|
|
2991
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
2992
|
+
.expect("clock should be after epoch")
|
|
2993
|
+
.as_nanos();
|
|
2994
|
+
let path = std::env::temp_dir().join(format!(
|
|
2995
|
+
"electron-cli-make-{label}-{}-{nanos}",
|
|
2996
|
+
std::process::id()
|
|
2997
|
+
));
|
|
2998
|
+
fs::create_dir_all(&path).expect("temp dir should be created");
|
|
2999
|
+
path
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
fn read_ar_members(path: &Path) -> std::collections::BTreeMap<String, Vec<u8>> {
|
|
3003
|
+
let bytes = fs::read(path).expect("ar archive should be readable");
|
|
3004
|
+
assert_eq!(&bytes[..8], b"!<arch>\n");
|
|
3005
|
+
|
|
3006
|
+
let mut members = std::collections::BTreeMap::new();
|
|
3007
|
+
let mut offset = 8;
|
|
3008
|
+
while offset < bytes.len() {
|
|
3009
|
+
let header = &bytes[offset..offset + 60];
|
|
3010
|
+
let name = std::str::from_utf8(&header[0..16])
|
|
3011
|
+
.expect("member name should be utf-8")
|
|
3012
|
+
.trim()
|
|
3013
|
+
.trim_end_matches('/')
|
|
3014
|
+
.to_string();
|
|
3015
|
+
let size = std::str::from_utf8(&header[48..58])
|
|
3016
|
+
.expect("member size should be utf-8")
|
|
3017
|
+
.trim()
|
|
3018
|
+
.parse::<usize>()
|
|
3019
|
+
.expect("member size should parse");
|
|
3020
|
+
let data_start = offset + 60;
|
|
3021
|
+
let data_end = data_start + size;
|
|
3022
|
+
members.insert(name, bytes[data_start..data_end].to_vec());
|
|
3023
|
+
offset = data_end + (size % 2);
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
members
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
fn read_tar_file(archive: &[u8], path: &str) -> String {
|
|
3030
|
+
let decoder = flate2::read::GzDecoder::new(archive);
|
|
3031
|
+
let mut archive = tar::Archive::new(decoder);
|
|
3032
|
+
for entry in archive.entries().expect("tar entries should read") {
|
|
3033
|
+
let mut entry = entry.expect("tar entry should read");
|
|
3034
|
+
let entry_path = entry
|
|
3035
|
+
.path()
|
|
3036
|
+
.expect("tar path should read")
|
|
3037
|
+
.to_string_lossy()
|
|
3038
|
+
.trim_start_matches("./")
|
|
3039
|
+
.to_string();
|
|
3040
|
+
if entry_path == path {
|
|
3041
|
+
let mut contents = String::new();
|
|
3042
|
+
entry
|
|
3043
|
+
.read_to_string(&mut contents)
|
|
3044
|
+
.expect("tar file should read");
|
|
3045
|
+
return contents;
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
panic!("tar file was not found: {path}");
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
fn tar_contains(archive: &[u8], path: &str) -> bool {
|
|
3053
|
+
let decoder = flate2::read::GzDecoder::new(archive);
|
|
3054
|
+
let mut archive = tar::Archive::new(decoder);
|
|
3055
|
+
archive
|
|
3056
|
+
.entries()
|
|
3057
|
+
.expect("tar entries should read")
|
|
3058
|
+
.any(|entry| {
|
|
3059
|
+
entry
|
|
3060
|
+
.expect("tar entry should read")
|
|
3061
|
+
.path()
|
|
3062
|
+
.expect("tar path should read")
|
|
3063
|
+
.to_string_lossy()
|
|
3064
|
+
.trim_start_matches("./")
|
|
3065
|
+
== path
|
|
3066
|
+
})
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
fn msi_rows(installer: &mut msi::Package<File>, table: &str) -> Vec<Vec<Value>> {
|
|
3070
|
+
installer
|
|
3071
|
+
.select_rows(msi::Select::table(table))
|
|
3072
|
+
.expect("msi rows should select")
|
|
3073
|
+
.map(|row| (0..row.len()).map(|index| row[index].clone()).collect())
|
|
3074
|
+
.collect()
|
|
3075
|
+
}
|
|
3076
|
+
}
|