electron-cli 0.3.0-alpha.11 → 0.3.0-alpha.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +77 -1
- package/Cargo.toml +4 -1
- package/README.md +3 -2
- package/package.json +1 -1
- package/src/cli.rs +2 -0
- package/src/commands/make.rs +897 -0
package/Cargo.lock
CHANGED
|
@@ -144,6 +144,18 @@ version = "1.11.1"
|
|
|
144
144
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
145
145
|
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
|
146
146
|
|
|
147
|
+
[[package]]
|
|
148
|
+
name = "cab"
|
|
149
|
+
version = "0.6.0"
|
|
150
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
151
|
+
checksum = "171228650e6721d5acc0868a462cd864f49ac5f64e4a42cde270406e64e404d2"
|
|
152
|
+
dependencies = [
|
|
153
|
+
"byteorder",
|
|
154
|
+
"flate2",
|
|
155
|
+
"lzxd",
|
|
156
|
+
"time",
|
|
157
|
+
]
|
|
158
|
+
|
|
147
159
|
[[package]]
|
|
148
160
|
name = "camino"
|
|
149
161
|
version = "1.2.2"
|
|
@@ -163,6 +175,17 @@ dependencies = [
|
|
|
163
175
|
"shlex",
|
|
164
176
|
]
|
|
165
177
|
|
|
178
|
+
[[package]]
|
|
179
|
+
name = "cfb"
|
|
180
|
+
version = "0.14.0"
|
|
181
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
182
|
+
checksum = "a347dcabdae9c31b0825fd6a8bed285ec9c2acb89c47827126d52fa4f59cece3"
|
|
183
|
+
dependencies = [
|
|
184
|
+
"fnv",
|
|
185
|
+
"uuid",
|
|
186
|
+
"web-time",
|
|
187
|
+
]
|
|
188
|
+
|
|
166
189
|
[[package]]
|
|
167
190
|
name = "cfg-if"
|
|
168
191
|
version = "1.0.4"
|
|
@@ -353,25 +376,37 @@ checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
|
|
|
353
376
|
|
|
354
377
|
[[package]]
|
|
355
378
|
name = "electron-cli"
|
|
356
|
-
version = "0.3.0-alpha.
|
|
379
|
+
version = "0.3.0-alpha.12"
|
|
357
380
|
dependencies = [
|
|
358
381
|
"anyhow",
|
|
359
382
|
"apple-dmg",
|
|
383
|
+
"cab",
|
|
360
384
|
"camino",
|
|
361
385
|
"clap",
|
|
362
386
|
"fatfs",
|
|
363
387
|
"flate2",
|
|
364
388
|
"fscommon",
|
|
365
389
|
"md5",
|
|
390
|
+
"msi",
|
|
366
391
|
"plist",
|
|
367
392
|
"rpm",
|
|
368
393
|
"serde",
|
|
369
394
|
"serde_json",
|
|
370
395
|
"tar",
|
|
371
396
|
"ureq",
|
|
397
|
+
"uuid",
|
|
372
398
|
"zip",
|
|
373
399
|
]
|
|
374
400
|
|
|
401
|
+
[[package]]
|
|
402
|
+
name = "encoding_rs"
|
|
403
|
+
version = "0.8.35"
|
|
404
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
405
|
+
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
|
406
|
+
dependencies = [
|
|
407
|
+
"cfg-if",
|
|
408
|
+
]
|
|
409
|
+
|
|
375
410
|
[[package]]
|
|
376
411
|
name = "enum-display-derive"
|
|
377
412
|
version = "0.1.1"
|
|
@@ -449,6 +484,12 @@ dependencies = [
|
|
|
449
484
|
"zlib-rs",
|
|
450
485
|
]
|
|
451
486
|
|
|
487
|
+
[[package]]
|
|
488
|
+
name = "fnv"
|
|
489
|
+
version = "1.0.7"
|
|
490
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
491
|
+
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
|
492
|
+
|
|
452
493
|
[[package]]
|
|
453
494
|
name = "foldhash"
|
|
454
495
|
version = "0.1.5"
|
|
@@ -809,6 +850,12 @@ version = "0.4.30"
|
|
|
809
850
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
810
851
|
checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
|
|
811
852
|
|
|
853
|
+
[[package]]
|
|
854
|
+
name = "lzxd"
|
|
855
|
+
version = "0.2.6"
|
|
856
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
857
|
+
checksum = "7b29dffab797218e12e4df08ef5d15ab9efca2504038b1b32b9b32fc844b39c9"
|
|
858
|
+
|
|
812
859
|
[[package]]
|
|
813
860
|
name = "md5"
|
|
814
861
|
version = "0.7.0"
|
|
@@ -831,6 +878,18 @@ dependencies = [
|
|
|
831
878
|
"simd-adler32",
|
|
832
879
|
]
|
|
833
880
|
|
|
881
|
+
[[package]]
|
|
882
|
+
name = "msi"
|
|
883
|
+
version = "0.10.0"
|
|
884
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
885
|
+
checksum = "b0325f8473ef1f5c38ee42345e2cd1678299cbbfa169d1776654a2a682867420"
|
|
886
|
+
dependencies = [
|
|
887
|
+
"byteorder",
|
|
888
|
+
"cfb",
|
|
889
|
+
"encoding_rs",
|
|
890
|
+
"uuid",
|
|
891
|
+
]
|
|
892
|
+
|
|
834
893
|
[[package]]
|
|
835
894
|
name = "nom"
|
|
836
895
|
version = "8.0.0"
|
|
@@ -1197,6 +1256,12 @@ dependencies = [
|
|
|
1197
1256
|
"digest",
|
|
1198
1257
|
]
|
|
1199
1258
|
|
|
1259
|
+
[[package]]
|
|
1260
|
+
name = "sha1_smol"
|
|
1261
|
+
version = "1.0.1"
|
|
1262
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1263
|
+
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
|
|
1264
|
+
|
|
1200
1265
|
[[package]]
|
|
1201
1266
|
name = "sha2"
|
|
1202
1267
|
version = "0.10.9"
|
|
@@ -1471,6 +1536,7 @@ checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7"
|
|
|
1471
1536
|
dependencies = [
|
|
1472
1537
|
"getrandom 0.4.2",
|
|
1473
1538
|
"js-sys",
|
|
1539
|
+
"sha1_smol",
|
|
1474
1540
|
"wasm-bindgen",
|
|
1475
1541
|
]
|
|
1476
1542
|
|
|
@@ -1583,6 +1649,16 @@ dependencies = [
|
|
|
1583
1649
|
"semver",
|
|
1584
1650
|
]
|
|
1585
1651
|
|
|
1652
|
+
[[package]]
|
|
1653
|
+
name = "web-time"
|
|
1654
|
+
version = "1.1.0"
|
|
1655
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1656
|
+
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
|
1657
|
+
dependencies = [
|
|
1658
|
+
"js-sys",
|
|
1659
|
+
"wasm-bindgen",
|
|
1660
|
+
]
|
|
1661
|
+
|
|
1586
1662
|
[[package]]
|
|
1587
1663
|
name = "webpki-roots"
|
|
1588
1664
|
version = "1.0.7"
|
package/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "electron-cli"
|
|
3
|
-
version = "0.3.0-alpha.
|
|
3
|
+
version = "0.3.0-alpha.12"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
description = "Experimental Rust CLI for Electron project diagnostics and workflow automation"
|
|
6
6
|
license = "MIT"
|
|
@@ -9,16 +9,19 @@ repository = "https://github.com/Ikana/electron-cli"
|
|
|
9
9
|
[dependencies]
|
|
10
10
|
anyhow = "1.0"
|
|
11
11
|
apple-dmg = "0.5"
|
|
12
|
+
cab = "0.6"
|
|
12
13
|
camino = { version = "1.1", features = ["serde1"] }
|
|
13
14
|
clap = { version = "4.6", features = ["derive"] }
|
|
14
15
|
fatfs = "0.3"
|
|
15
16
|
flate2 = { version = "1.1", default-features = false, features = ["rust_backend"] }
|
|
16
17
|
fscommon = "0.1"
|
|
17
18
|
md5 = "0.7"
|
|
19
|
+
msi = "0.10"
|
|
18
20
|
plist = "1"
|
|
19
21
|
rpm = { version = "0.24", default-features = false, features = ["payload", "gzip-compression"] }
|
|
20
22
|
serde = { version = "1.0", features = ["derive"] }
|
|
21
23
|
serde_json = "1.0"
|
|
22
24
|
tar = "0.4"
|
|
23
25
|
ureq = { version = "3.3", features = ["json"] }
|
|
26
|
+
uuid = { version = "1", features = ["v5"] }
|
|
24
27
|
zip = { version = "8.6.0", default-features = false, features = ["deflate-flate2-zlib-rs"] }
|
package/README.md
CHANGED
|
@@ -36,12 +36,12 @@ The Rust-native flow currently owns:
|
|
|
36
36
|
- `init --template minimal`: writes a local Electron starter without Electron Forge.
|
|
37
37
|
- `start`: launches the installed Electron runtime directly.
|
|
38
38
|
- `package`: copies the installed Electron runtime, app files, installed production dependency closure, app metadata, macOS icon, and extra resources into a local app bundle for the current platform and architecture.
|
|
39
|
-
- `make`: runs `package` and writes a distributable under `out/make/<target>/<platform>/<arch>/`; ZIP works on all platforms, `--target dmg` writes a basic macOS disk image,
|
|
39
|
+
- `make`: runs `package` and writes a distributable under `out/make/<target>/<platform>/<arch>/`; ZIP works on all platforms, `--target dmg` writes a basic macOS disk image, `--target deb` / `--target rpm` write Linux packages, and `--target msi` writes a basic Windows Installer package.
|
|
40
40
|
- `publish`: runs `make` and publishes the distributable to a local directory with a manifest or to GitHub Releases.
|
|
41
41
|
|
|
42
42
|
The GitHub publisher creates or reuses a release, uploads the selected make artifact, and can replace an existing asset with `--force`. It reads `GITHUB_TOKEN` or `GH_TOKEN` and can infer `OWNER/REPO` from `package.json` `repository`, or you can pass `--github-repo`.
|
|
43
43
|
|
|
44
|
-
The DMG maker is currently a pure-Rust FAT32 image with the app bundle and an Applications entry. HFS+/APFS layout customization,
|
|
44
|
+
The DMG maker is currently a pure-Rust FAT32 image with the app bundle and an Applications entry. The MSI maker writes a compressed embedded CAB, Windows Installer database tables, and a Start Menu shortcut when the packaged executable is present. HFS+/APFS DMG layout customization, installer UI customization, Windows/Linux icon embedding, signing, and notarization are still TODO.
|
|
45
45
|
|
|
46
46
|
Package metadata can be configured in `package.json`:
|
|
47
47
|
|
|
@@ -97,6 +97,7 @@ cargo run -- make --dry-run
|
|
|
97
97
|
cargo run -- make --target dmg --dry-run
|
|
98
98
|
cargo run -- make --target deb --dry-run
|
|
99
99
|
cargo run -- make --target rpm --dry-run
|
|
100
|
+
cargo run -- make --target msi --platform win32 --dry-run
|
|
100
101
|
cargo run -- publish --dry-run
|
|
101
102
|
cargo run -- publish --publisher github --dry-run
|
|
102
103
|
cargo run -- publish --publisher github --github-repo OWNER/REPO --github-tag v0.1.0
|
package/package.json
CHANGED
package/src/cli.rs
CHANGED
|
@@ -289,6 +289,7 @@ impl PackageManager {
|
|
|
289
289
|
pub enum MakeTarget {
|
|
290
290
|
Deb,
|
|
291
291
|
Dmg,
|
|
292
|
+
Msi,
|
|
292
293
|
Rpm,
|
|
293
294
|
Zip,
|
|
294
295
|
}
|
|
@@ -298,6 +299,7 @@ impl MakeTarget {
|
|
|
298
299
|
match self {
|
|
299
300
|
MakeTarget::Deb => "deb",
|
|
300
301
|
MakeTarget::Dmg => "dmg",
|
|
302
|
+
MakeTarget::Msi => "msi",
|
|
301
303
|
MakeTarget::Rpm => "rpm",
|
|
302
304
|
MakeTarget::Zip => "zip",
|
|
303
305
|
}
|
package/src/commands/make.rs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
use std::{
|
|
2
|
+
collections::BTreeMap,
|
|
2
3
|
fs,
|
|
3
4
|
fs::File,
|
|
4
5
|
io::{self, BufWriter, Cursor, Write},
|
|
@@ -6,13 +7,16 @@ use std::{
|
|
|
6
7
|
};
|
|
7
8
|
|
|
8
9
|
use anyhow::{bail, Context, Result};
|
|
10
|
+
use cab::{CabinetBuilder, CompressionType as CabCompressionType};
|
|
9
11
|
use camino::Utf8PathBuf;
|
|
10
12
|
use fatfs::{Dir as FatDir, FileSystem, FormatVolumeOptions, FsOptions, ReadWriteSeek};
|
|
11
13
|
use flate2::{write::GzEncoder, Compression};
|
|
12
14
|
use fscommon::BufStream;
|
|
15
|
+
use msi::{Column, Insert, Language, Package, PackageType, Value};
|
|
13
16
|
use rpm::{BuildConfig, CompressionType, FileOptions, PackageBuilder};
|
|
14
17
|
use serde::Serialize;
|
|
15
18
|
use tar::{Builder as TarBuilder, Header as TarHeader};
|
|
19
|
+
use uuid::Uuid;
|
|
16
20
|
use zip::{write::SimpleFileOptions, CompressionMethod, ZipWriter};
|
|
17
21
|
|
|
18
22
|
use crate::{
|
|
@@ -88,6 +92,12 @@ pub(crate) fn build_report(args: &MakeArgs) -> Result<MakeReport> {
|
|
|
88
92
|
package.platform()
|
|
89
93
|
));
|
|
90
94
|
}
|
|
95
|
+
if args.target == MakeTarget::Msi && package.platform() != "win32" {
|
|
96
|
+
warnings.push(format!(
|
|
97
|
+
"msi maker only supports Windows packages; target platform is {}.",
|
|
98
|
+
package.platform()
|
|
99
|
+
));
|
|
100
|
+
}
|
|
91
101
|
if args.skip_package && !Path::new(package.bundle_dir().as_str()).exists() {
|
|
92
102
|
warnings.push(format!(
|
|
93
103
|
"Package output does not exist: {}.",
|
|
@@ -147,6 +157,7 @@ pub(crate) fn execute_make(report: &mut MakeReport, args: &MakeArgs) -> Result<(
|
|
|
147
157
|
}
|
|
148
158
|
MakeTarget::Deb => write_deb_archive(&report.package, artifact)?,
|
|
149
159
|
MakeTarget::Dmg => write_dmg_archive(&report.package, artifact)?,
|
|
160
|
+
MakeTarget::Msi => write_msi_archive(&report.package, artifact)?,
|
|
150
161
|
MakeTarget::Rpm => write_rpm_archive(&report.package, artifact)?,
|
|
151
162
|
}
|
|
152
163
|
|
|
@@ -213,6 +224,12 @@ fn make_artifact_path(make_dir: &Path, package: &PackageReport, target: MakeTarg
|
|
|
213
224
|
dmg_version(package.project().version.as_deref()),
|
|
214
225
|
package.arch()
|
|
215
226
|
)),
|
|
227
|
+
MakeTarget::Msi => make_dir.join(format!(
|
|
228
|
+
"{}-{}-{}.msi",
|
|
229
|
+
package.artifact_stem(),
|
|
230
|
+
windows_artifact_version(package.project().version.as_deref()),
|
|
231
|
+
windows_arch(package.arch())
|
|
232
|
+
)),
|
|
216
233
|
MakeTarget::Rpm => make_dir.join(format!(
|
|
217
234
|
"{}-{}-1.{}.rpm",
|
|
218
235
|
rpm_package_name(&package.artifact_stem()),
|
|
@@ -655,6 +672,627 @@ fn write_rpm_archive(package: &PackageReport, artifact: &Path) -> Result<()> {
|
|
|
655
672
|
.with_context(|| format!("Could not write {}", artifact.display()))
|
|
656
673
|
}
|
|
657
674
|
|
|
675
|
+
#[derive(Debug)]
|
|
676
|
+
struct MsiPayload {
|
|
677
|
+
directories: Vec<MsiDirectoryEntry>,
|
|
678
|
+
files: Vec<MsiFileEntry>,
|
|
679
|
+
shortcut_component: Option<String>,
|
|
680
|
+
shortcut_target_file: Option<String>,
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
#[derive(Debug)]
|
|
684
|
+
struct MsiDirectoryEntry {
|
|
685
|
+
id: String,
|
|
686
|
+
parent_id: String,
|
|
687
|
+
name: String,
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
#[derive(Debug)]
|
|
691
|
+
struct MsiFileEntry {
|
|
692
|
+
id: String,
|
|
693
|
+
component_id: String,
|
|
694
|
+
component_guid: String,
|
|
695
|
+
directory_id: String,
|
|
696
|
+
source: PathBuf,
|
|
697
|
+
file_name: String,
|
|
698
|
+
cabinet_name: String,
|
|
699
|
+
size: i32,
|
|
700
|
+
sequence: i32,
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
fn write_msi_archive(package: &PackageReport, artifact: &Path) -> Result<()> {
|
|
704
|
+
if package.platform() != "win32" {
|
|
705
|
+
bail!(
|
|
706
|
+
"MSI maker only supports Windows packages. Requested {}.",
|
|
707
|
+
package.platform()
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
let source = Path::new(package.bundle_dir().as_str());
|
|
712
|
+
if !source.exists() {
|
|
713
|
+
bail!("Package output does not exist: {}", source.display());
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
let parent = artifact
|
|
717
|
+
.parent()
|
|
718
|
+
.with_context(|| format!("Artifact path has no parent: {}", artifact.display()))?;
|
|
719
|
+
fs::create_dir_all(parent).with_context(|| format!("Could not create {}", parent.display()))?;
|
|
720
|
+
|
|
721
|
+
let payload = collect_msi_payload(package, source)?;
|
|
722
|
+
if payload.files.is_empty() {
|
|
723
|
+
bail!(
|
|
724
|
+
"MSI maker requires at least one packaged file in {}",
|
|
725
|
+
source.display()
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
let cabinet = create_msi_cabinet(&payload)?;
|
|
730
|
+
let file = fs::OpenOptions::new()
|
|
731
|
+
.read(true)
|
|
732
|
+
.write(true)
|
|
733
|
+
.create(true)
|
|
734
|
+
.truncate(true)
|
|
735
|
+
.open(artifact)
|
|
736
|
+
.with_context(|| format!("Could not create {}", artifact.display()))?;
|
|
737
|
+
let mut installer =
|
|
738
|
+
Package::create(PackageType::Installer, file).context("Could not create MSI package")?;
|
|
739
|
+
|
|
740
|
+
write_msi_summary(&mut installer, package)?;
|
|
741
|
+
create_msi_tables(&mut installer)?;
|
|
742
|
+
insert_msi_rows(&mut installer, package, &payload)?;
|
|
743
|
+
{
|
|
744
|
+
let mut stream = installer
|
|
745
|
+
.write_stream("app.cab")
|
|
746
|
+
.context("Could not create embedded MSI cabinet stream")?;
|
|
747
|
+
stream
|
|
748
|
+
.write_all(&cabinet)
|
|
749
|
+
.context("Could not write embedded MSI cabinet stream")?;
|
|
750
|
+
}
|
|
751
|
+
installer.flush().context("Could not flush MSI package")?;
|
|
752
|
+
installer
|
|
753
|
+
.into_inner()
|
|
754
|
+
.context("Could not finish MSI package")?;
|
|
755
|
+
|
|
756
|
+
Ok(())
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
fn write_msi_summary(installer: &mut Package<File>, package: &PackageReport) -> Result<()> {
|
|
760
|
+
let package_code = deterministic_guid(
|
|
761
|
+
"package-code",
|
|
762
|
+
&[
|
|
763
|
+
package.app_name(),
|
|
764
|
+
package.project().version.as_deref().unwrap_or("0.1.0"),
|
|
765
|
+
package.arch(),
|
|
766
|
+
],
|
|
767
|
+
);
|
|
768
|
+
let arch = msi_summary_arch(package.arch());
|
|
769
|
+
let language = Language::from_code(1033);
|
|
770
|
+
let summary = installer.summary_info_mut();
|
|
771
|
+
summary.set_title(format!("{} Installer", package.app_name()));
|
|
772
|
+
summary.set_subject(package.app_name().to_string());
|
|
773
|
+
summary.set_author("electron-cli".to_string());
|
|
774
|
+
summary.set_comments(format!(
|
|
775
|
+
"{} packaged by electron-cli.",
|
|
776
|
+
single_line(package.app_name())
|
|
777
|
+
));
|
|
778
|
+
summary.set_creating_application("electron-cli".to_string());
|
|
779
|
+
summary.set_uuid(package_code);
|
|
780
|
+
summary.set_arch(arch.to_string());
|
|
781
|
+
summary.set_languages(&[language]);
|
|
782
|
+
summary.set_page_count(200);
|
|
783
|
+
summary.set_word_count(2);
|
|
784
|
+
Ok(())
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
fn create_msi_tables(installer: &mut Package<File>) -> Result<()> {
|
|
788
|
+
create_msi_table(
|
|
789
|
+
installer,
|
|
790
|
+
"Property",
|
|
791
|
+
vec![
|
|
792
|
+
Column::build("Property").primary_key().id_string(72),
|
|
793
|
+
Column::build("Value").nullable().formatted_string(0),
|
|
794
|
+
],
|
|
795
|
+
)?;
|
|
796
|
+
create_msi_table(
|
|
797
|
+
installer,
|
|
798
|
+
"Directory",
|
|
799
|
+
vec![
|
|
800
|
+
Column::build("Directory").primary_key().id_string(72),
|
|
801
|
+
Column::build("Directory_Parent").nullable().id_string(72),
|
|
802
|
+
Column::build("DefaultDir").text_string(255),
|
|
803
|
+
],
|
|
804
|
+
)?;
|
|
805
|
+
create_msi_table(
|
|
806
|
+
installer,
|
|
807
|
+
"Feature",
|
|
808
|
+
vec![
|
|
809
|
+
Column::build("Feature").primary_key().id_string(38),
|
|
810
|
+
Column::build("Feature_Parent").nullable().id_string(38),
|
|
811
|
+
Column::build("Title").nullable().text_string(64),
|
|
812
|
+
Column::build("Description").nullable().text_string(255),
|
|
813
|
+
Column::build("Display").nullable().int16(),
|
|
814
|
+
Column::build("Level").int16(),
|
|
815
|
+
Column::build("Directory_").nullable().id_string(72),
|
|
816
|
+
Column::build("Attributes").int16(),
|
|
817
|
+
],
|
|
818
|
+
)?;
|
|
819
|
+
create_msi_table(
|
|
820
|
+
installer,
|
|
821
|
+
"Component",
|
|
822
|
+
vec![
|
|
823
|
+
Column::build("Component").primary_key().id_string(72),
|
|
824
|
+
Column::build("ComponentId").nullable().string(38),
|
|
825
|
+
Column::build("Directory_").id_string(72),
|
|
826
|
+
Column::build("Attributes").int16(),
|
|
827
|
+
Column::build("Condition").nullable().formatted_string(255),
|
|
828
|
+
Column::build("KeyPath").nullable().id_string(72),
|
|
829
|
+
],
|
|
830
|
+
)?;
|
|
831
|
+
create_msi_table(
|
|
832
|
+
installer,
|
|
833
|
+
"FeatureComponents",
|
|
834
|
+
vec![
|
|
835
|
+
Column::build("Feature_").primary_key().id_string(38),
|
|
836
|
+
Column::build("Component_").primary_key().id_string(72),
|
|
837
|
+
],
|
|
838
|
+
)?;
|
|
839
|
+
create_msi_table(
|
|
840
|
+
installer,
|
|
841
|
+
"File",
|
|
842
|
+
vec![
|
|
843
|
+
Column::build("File").primary_key().id_string(72),
|
|
844
|
+
Column::build("Component_").id_string(72),
|
|
845
|
+
Column::build("FileName").text_string(255),
|
|
846
|
+
Column::build("FileSize").int32(),
|
|
847
|
+
Column::build("Version").nullable().string(72),
|
|
848
|
+
Column::build("Language").nullable().string(20),
|
|
849
|
+
Column::build("Attributes").nullable().int16(),
|
|
850
|
+
Column::build("Sequence").int16(),
|
|
851
|
+
],
|
|
852
|
+
)?;
|
|
853
|
+
create_msi_table(
|
|
854
|
+
installer,
|
|
855
|
+
"Media",
|
|
856
|
+
vec![
|
|
857
|
+
Column::build("DiskId").primary_key().int16(),
|
|
858
|
+
Column::build("LastSequence").int16(),
|
|
859
|
+
Column::build("DiskPrompt").nullable().text_string(64),
|
|
860
|
+
Column::build("Cabinet").nullable().string(255),
|
|
861
|
+
Column::build("VolumeLabel").nullable().string(32),
|
|
862
|
+
Column::build("Source").nullable().string(72),
|
|
863
|
+
],
|
|
864
|
+
)?;
|
|
865
|
+
create_msi_table(
|
|
866
|
+
installer,
|
|
867
|
+
"Shortcut",
|
|
868
|
+
vec![
|
|
869
|
+
Column::build("Shortcut").primary_key().id_string(72),
|
|
870
|
+
Column::build("Directory_").id_string(72),
|
|
871
|
+
Column::build("Name").text_string(128),
|
|
872
|
+
Column::build("Component_").id_string(72),
|
|
873
|
+
Column::build("Target").formatted_string(0),
|
|
874
|
+
Column::build("Arguments").nullable().formatted_string(255),
|
|
875
|
+
Column::build("Description").nullable().text_string(255),
|
|
876
|
+
Column::build("Hotkey").nullable().int16(),
|
|
877
|
+
Column::build("Icon_").nullable().id_string(72),
|
|
878
|
+
Column::build("IconIndex").nullable().int16(),
|
|
879
|
+
Column::build("ShowCmd").nullable().int16(),
|
|
880
|
+
Column::build("WkDir").nullable().id_string(72),
|
|
881
|
+
],
|
|
882
|
+
)?;
|
|
883
|
+
create_msi_table(
|
|
884
|
+
installer,
|
|
885
|
+
"RemoveFile",
|
|
886
|
+
vec![
|
|
887
|
+
Column::build("FileKey").primary_key().id_string(72),
|
|
888
|
+
Column::build("Component_").id_string(72),
|
|
889
|
+
Column::build("FileName").nullable().text_string(255),
|
|
890
|
+
Column::build("DirProperty").id_string(72),
|
|
891
|
+
Column::build("InstallMode").int16(),
|
|
892
|
+
],
|
|
893
|
+
)?;
|
|
894
|
+
create_msi_table(
|
|
895
|
+
installer,
|
|
896
|
+
"InstallExecuteSequence",
|
|
897
|
+
vec![
|
|
898
|
+
Column::build("Action").primary_key().id_string(72),
|
|
899
|
+
Column::build("Condition").nullable().formatted_string(255),
|
|
900
|
+
Column::build("Sequence").nullable().int16(),
|
|
901
|
+
],
|
|
902
|
+
)?;
|
|
903
|
+
create_msi_table(
|
|
904
|
+
installer,
|
|
905
|
+
"ActionText",
|
|
906
|
+
vec![
|
|
907
|
+
Column::build("Action").primary_key().id_string(72),
|
|
908
|
+
Column::build("Description").nullable().text_string(64),
|
|
909
|
+
Column::build("Template").nullable().formatted_string(128),
|
|
910
|
+
],
|
|
911
|
+
)
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
fn create_msi_table(installer: &mut Package<File>, name: &str, columns: Vec<Column>) -> Result<()> {
|
|
915
|
+
installer
|
|
916
|
+
.create_table(name, columns)
|
|
917
|
+
.with_context(|| format!("Could not create MSI {name} table"))
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
fn insert_msi_rows(
|
|
921
|
+
installer: &mut Package<File>,
|
|
922
|
+
package: &PackageReport,
|
|
923
|
+
payload: &MsiPayload,
|
|
924
|
+
) -> Result<()> {
|
|
925
|
+
let product_version = msi_product_version(package.project().version.as_deref());
|
|
926
|
+
let product_code = msi_guid(deterministic_guid(
|
|
927
|
+
"product-code",
|
|
928
|
+
&[package.app_name(), &product_version, package.arch()],
|
|
929
|
+
));
|
|
930
|
+
let upgrade_code = msi_guid(deterministic_guid(
|
|
931
|
+
"upgrade-code",
|
|
932
|
+
&[
|
|
933
|
+
package.app_name(),
|
|
934
|
+
package.project().name.as_deref().unwrap_or(""),
|
|
935
|
+
],
|
|
936
|
+
));
|
|
937
|
+
insert_msi_table_rows(
|
|
938
|
+
installer,
|
|
939
|
+
"Property",
|
|
940
|
+
vec![
|
|
941
|
+
vec![s("ProductCode"), s(product_code)],
|
|
942
|
+
vec![s("ProductLanguage"), s("1033")],
|
|
943
|
+
vec![s("ProductName"), s(package.app_name())],
|
|
944
|
+
vec![s("ProductVersion"), s(product_version)],
|
|
945
|
+
vec![s("Manufacturer"), s("electron-cli")],
|
|
946
|
+
vec![s("UpgradeCode"), s(upgrade_code)],
|
|
947
|
+
vec![s("ALLUSERS"), s("1")],
|
|
948
|
+
vec![s("INSTALLLEVEL"), s("1")],
|
|
949
|
+
],
|
|
950
|
+
)?;
|
|
951
|
+
|
|
952
|
+
let program_files_dir = msi_program_files_directory(package.arch());
|
|
953
|
+
let install_folder = msi_filename("APPDIR", package.app_name());
|
|
954
|
+
insert_msi_table_rows(
|
|
955
|
+
installer,
|
|
956
|
+
"Directory",
|
|
957
|
+
vec![
|
|
958
|
+
vec![s("TARGETDIR"), Value::Null, s("SourceDir")],
|
|
959
|
+
vec![s(program_files_dir), s("TARGETDIR"), s(".")],
|
|
960
|
+
vec![s("INSTALLFOLDER"), s(program_files_dir), s(install_folder)],
|
|
961
|
+
vec![s("ProgramMenuFolder"), s("TARGETDIR"), s(".")],
|
|
962
|
+
vec![
|
|
963
|
+
s("ApplicationProgramsFolder"),
|
|
964
|
+
s("ProgramMenuFolder"),
|
|
965
|
+
s(msi_filename("APPMENU", package.app_name())),
|
|
966
|
+
],
|
|
967
|
+
],
|
|
968
|
+
)?;
|
|
969
|
+
insert_msi_table_rows(
|
|
970
|
+
installer,
|
|
971
|
+
"Directory",
|
|
972
|
+
payload
|
|
973
|
+
.directories
|
|
974
|
+
.iter()
|
|
975
|
+
.map(|directory| {
|
|
976
|
+
vec![
|
|
977
|
+
s(&directory.id),
|
|
978
|
+
s(&directory.parent_id),
|
|
979
|
+
s(&directory.name),
|
|
980
|
+
]
|
|
981
|
+
})
|
|
982
|
+
.collect(),
|
|
983
|
+
)?;
|
|
984
|
+
|
|
985
|
+
insert_msi_table_rows(
|
|
986
|
+
installer,
|
|
987
|
+
"Feature",
|
|
988
|
+
vec![vec![
|
|
989
|
+
s("MainFeature"),
|
|
990
|
+
Value::Null,
|
|
991
|
+
s(package.app_name()),
|
|
992
|
+
s(format!("Install {}.", single_line(package.app_name()))),
|
|
993
|
+
Value::from(1),
|
|
994
|
+
Value::from(1),
|
|
995
|
+
s("INSTALLFOLDER"),
|
|
996
|
+
Value::from(0),
|
|
997
|
+
]],
|
|
998
|
+
)?;
|
|
999
|
+
|
|
1000
|
+
let component_attributes = msi_component_attributes(package.arch());
|
|
1001
|
+
insert_msi_table_rows(
|
|
1002
|
+
installer,
|
|
1003
|
+
"Component",
|
|
1004
|
+
payload
|
|
1005
|
+
.files
|
|
1006
|
+
.iter()
|
|
1007
|
+
.map(|file| {
|
|
1008
|
+
vec![
|
|
1009
|
+
s(&file.component_id),
|
|
1010
|
+
s(&file.component_guid),
|
|
1011
|
+
s(&file.directory_id),
|
|
1012
|
+
Value::from(component_attributes),
|
|
1013
|
+
Value::Null,
|
|
1014
|
+
s(&file.id),
|
|
1015
|
+
]
|
|
1016
|
+
})
|
|
1017
|
+
.collect(),
|
|
1018
|
+
)?;
|
|
1019
|
+
insert_msi_table_rows(
|
|
1020
|
+
installer,
|
|
1021
|
+
"FeatureComponents",
|
|
1022
|
+
payload
|
|
1023
|
+
.files
|
|
1024
|
+
.iter()
|
|
1025
|
+
.map(|file| vec![s("MainFeature"), s(&file.component_id)])
|
|
1026
|
+
.collect(),
|
|
1027
|
+
)?;
|
|
1028
|
+
insert_msi_table_rows(
|
|
1029
|
+
installer,
|
|
1030
|
+
"File",
|
|
1031
|
+
payload
|
|
1032
|
+
.files
|
|
1033
|
+
.iter()
|
|
1034
|
+
.map(|file| {
|
|
1035
|
+
vec![
|
|
1036
|
+
s(&file.id),
|
|
1037
|
+
s(&file.component_id),
|
|
1038
|
+
s(&file.file_name),
|
|
1039
|
+
Value::from(file.size),
|
|
1040
|
+
Value::Null,
|
|
1041
|
+
Value::Null,
|
|
1042
|
+
Value::from(0),
|
|
1043
|
+
Value::from(file.sequence),
|
|
1044
|
+
]
|
|
1045
|
+
})
|
|
1046
|
+
.collect(),
|
|
1047
|
+
)?;
|
|
1048
|
+
insert_msi_table_rows(
|
|
1049
|
+
installer,
|
|
1050
|
+
"Media",
|
|
1051
|
+
vec![vec![
|
|
1052
|
+
Value::from(1),
|
|
1053
|
+
Value::from(payload.files.len() as i32),
|
|
1054
|
+
Value::Null,
|
|
1055
|
+
s("#app.cab"),
|
|
1056
|
+
Value::Null,
|
|
1057
|
+
Value::Null,
|
|
1058
|
+
]],
|
|
1059
|
+
)?;
|
|
1060
|
+
|
|
1061
|
+
if let (Some(component), Some(target_file)) =
|
|
1062
|
+
(&payload.shortcut_component, &payload.shortcut_target_file)
|
|
1063
|
+
{
|
|
1064
|
+
insert_msi_table_rows(
|
|
1065
|
+
installer,
|
|
1066
|
+
"Shortcut",
|
|
1067
|
+
vec![vec![
|
|
1068
|
+
s("ApplicationShortcut"),
|
|
1069
|
+
s("ApplicationProgramsFolder"),
|
|
1070
|
+
s(msi_filename("SHORTCUT", package.app_name())),
|
|
1071
|
+
s(component),
|
|
1072
|
+
s(format!("[#{target_file}]")),
|
|
1073
|
+
Value::Null,
|
|
1074
|
+
s(format!("Launch {}.", single_line(package.app_name()))),
|
|
1075
|
+
Value::Null,
|
|
1076
|
+
Value::Null,
|
|
1077
|
+
Value::Null,
|
|
1078
|
+
Value::Null,
|
|
1079
|
+
s("INSTALLFOLDER"),
|
|
1080
|
+
]],
|
|
1081
|
+
)?;
|
|
1082
|
+
insert_msi_table_rows(
|
|
1083
|
+
installer,
|
|
1084
|
+
"RemoveFile",
|
|
1085
|
+
vec![vec![
|
|
1086
|
+
s("RemoveStartMenuFolder"),
|
|
1087
|
+
s(component),
|
|
1088
|
+
Value::Null,
|
|
1089
|
+
s("ApplicationProgramsFolder"),
|
|
1090
|
+
Value::from(2),
|
|
1091
|
+
]],
|
|
1092
|
+
)?;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
insert_msi_table_rows(
|
|
1096
|
+
installer,
|
|
1097
|
+
"InstallExecuteSequence",
|
|
1098
|
+
vec![
|
|
1099
|
+
standard_action("CostInitialize", 800),
|
|
1100
|
+
standard_action("FileCost", 900),
|
|
1101
|
+
standard_action("CostFinalize", 1000),
|
|
1102
|
+
standard_action("InstallValidate", 1400),
|
|
1103
|
+
standard_action("InstallInitialize", 1500),
|
|
1104
|
+
standard_action("ProcessComponents", 1600),
|
|
1105
|
+
standard_action("UnpublishFeatures", 1800),
|
|
1106
|
+
standard_action("RemoveShortcuts", 3200),
|
|
1107
|
+
standard_action("RemoveFiles", 3500),
|
|
1108
|
+
standard_action("InstallFiles", 4000),
|
|
1109
|
+
standard_action("CreateShortcuts", 4500),
|
|
1110
|
+
standard_action("RegisterUser", 6000),
|
|
1111
|
+
standard_action("RegisterProduct", 6100),
|
|
1112
|
+
standard_action("PublishFeatures", 6300),
|
|
1113
|
+
standard_action("PublishProduct", 6400),
|
|
1114
|
+
standard_action("InstallFinalize", 6600),
|
|
1115
|
+
],
|
|
1116
|
+
)?;
|
|
1117
|
+
insert_msi_table_rows(
|
|
1118
|
+
installer,
|
|
1119
|
+
"ActionText",
|
|
1120
|
+
vec![
|
|
1121
|
+
action_text(
|
|
1122
|
+
"InstallFiles",
|
|
1123
|
+
"Copying new files",
|
|
1124
|
+
"File: [1], Directory: [9], Size: [6]",
|
|
1125
|
+
),
|
|
1126
|
+
action_text("CreateShortcuts", "Creating shortcuts", "Shortcut: [1]"),
|
|
1127
|
+
action_text("RemoveFiles", "Removing files", "File: [1], Directory: [9]"),
|
|
1128
|
+
action_text("RemoveShortcuts", "Removing shortcuts", "Shortcut: [1]"),
|
|
1129
|
+
],
|
|
1130
|
+
)
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
fn insert_msi_table_rows(
|
|
1134
|
+
installer: &mut Package<File>,
|
|
1135
|
+
table: &str,
|
|
1136
|
+
rows: Vec<Vec<Value>>,
|
|
1137
|
+
) -> Result<()> {
|
|
1138
|
+
if rows.is_empty() {
|
|
1139
|
+
return Ok(());
|
|
1140
|
+
}
|
|
1141
|
+
installer
|
|
1142
|
+
.insert_rows(Insert::into(table).rows(rows))
|
|
1143
|
+
.with_context(|| format!("Could not insert MSI {table} rows"))
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
fn standard_action(action: &str, sequence: i32) -> Vec<Value> {
|
|
1147
|
+
vec![s(action), Value::Null, Value::from(sequence)]
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
fn action_text(action: &str, description: &str, template: &str) -> Vec<Value> {
|
|
1151
|
+
vec![s(action), s(description), s(template)]
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
fn collect_msi_payload(package: &PackageReport, source: &Path) -> Result<MsiPayload> {
|
|
1155
|
+
let mut payload = MsiPayload {
|
|
1156
|
+
directories: Vec::new(),
|
|
1157
|
+
files: Vec::new(),
|
|
1158
|
+
shortcut_component: None,
|
|
1159
|
+
shortcut_target_file: None,
|
|
1160
|
+
};
|
|
1161
|
+
let mut directory_ids = BTreeMap::from([(PathBuf::new(), "INSTALLFOLDER".to_string())]);
|
|
1162
|
+
collect_msi_directory(
|
|
1163
|
+
package,
|
|
1164
|
+
source,
|
|
1165
|
+
Path::new(""),
|
|
1166
|
+
"INSTALLFOLDER",
|
|
1167
|
+
&mut directory_ids,
|
|
1168
|
+
&mut payload,
|
|
1169
|
+
)?;
|
|
1170
|
+
|
|
1171
|
+
if payload.files.len() > i16::MAX as usize {
|
|
1172
|
+
bail!(
|
|
1173
|
+
"MSI maker supports up to {} files; package contains {}.",
|
|
1174
|
+
i16::MAX,
|
|
1175
|
+
payload.files.len()
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
Ok(payload)
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
fn collect_msi_directory(
|
|
1183
|
+
package: &PackageReport,
|
|
1184
|
+
source: &Path,
|
|
1185
|
+
relative_dir: &Path,
|
|
1186
|
+
directory_id: &str,
|
|
1187
|
+
directory_ids: &mut BTreeMap<PathBuf, String>,
|
|
1188
|
+
payload: &mut MsiPayload,
|
|
1189
|
+
) -> Result<()> {
|
|
1190
|
+
let mut entries = fs::read_dir(source)
|
|
1191
|
+
.with_context(|| format!("Could not read {}", source.display()))?
|
|
1192
|
+
.collect::<Result<Vec<_>, io::Error>>()?;
|
|
1193
|
+
entries.sort_by_key(|entry| entry.path());
|
|
1194
|
+
|
|
1195
|
+
for entry in entries {
|
|
1196
|
+
let path = entry.path();
|
|
1197
|
+
let file_name = utf8_file_name(&path)?.to_string();
|
|
1198
|
+
let relative_path = relative_dir.join(&file_name);
|
|
1199
|
+
let metadata = fs::symlink_metadata(&path)
|
|
1200
|
+
.with_context(|| format!("Could not stat {}", path.display()))?;
|
|
1201
|
+
|
|
1202
|
+
if metadata.file_type().is_symlink() {
|
|
1203
|
+
bail!(
|
|
1204
|
+
"MSI maker does not support symbolic links yet: {}",
|
|
1205
|
+
path.display()
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
if metadata.is_dir() {
|
|
1210
|
+
let dir_id = format!("D{:04}", directory_ids.len());
|
|
1211
|
+
directory_ids.insert(relative_path.clone(), dir_id.clone());
|
|
1212
|
+
payload.directories.push(MsiDirectoryEntry {
|
|
1213
|
+
id: dir_id.clone(),
|
|
1214
|
+
parent_id: directory_id.to_string(),
|
|
1215
|
+
name: msi_filename(&dir_id, &file_name),
|
|
1216
|
+
});
|
|
1217
|
+
collect_msi_directory(
|
|
1218
|
+
package,
|
|
1219
|
+
&path,
|
|
1220
|
+
&relative_path,
|
|
1221
|
+
&dir_id,
|
|
1222
|
+
directory_ids,
|
|
1223
|
+
payload,
|
|
1224
|
+
)?;
|
|
1225
|
+
} else if metadata.is_file() {
|
|
1226
|
+
let sequence = payload.files.len() + 1;
|
|
1227
|
+
let size = i32::try_from(metadata.len())
|
|
1228
|
+
.with_context(|| format!("MSI file is too large: {}", path.display()))?;
|
|
1229
|
+
let file_id = format!("F{sequence:04}");
|
|
1230
|
+
let component_id = format!("C{sequence:04}");
|
|
1231
|
+
let relative_key = relative_path.to_string_lossy().replace('\\', "/");
|
|
1232
|
+
let component_guid = msi_guid(deterministic_guid(
|
|
1233
|
+
"component",
|
|
1234
|
+
&[
|
|
1235
|
+
package.app_name(),
|
|
1236
|
+
package.project().name.as_deref().unwrap_or(""),
|
|
1237
|
+
&relative_key,
|
|
1238
|
+
],
|
|
1239
|
+
));
|
|
1240
|
+
let entry = MsiFileEntry {
|
|
1241
|
+
id: file_id.clone(),
|
|
1242
|
+
component_id: component_id.clone(),
|
|
1243
|
+
component_guid,
|
|
1244
|
+
directory_id: directory_id.to_string(),
|
|
1245
|
+
source: path.clone(),
|
|
1246
|
+
file_name: msi_filename(&file_id, &file_name),
|
|
1247
|
+
cabinet_name: file_id.clone(),
|
|
1248
|
+
size,
|
|
1249
|
+
sequence: sequence as i32,
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
if file_name.eq_ignore_ascii_case(package.executable_name()) {
|
|
1253
|
+
payload.shortcut_component = Some(component_id);
|
|
1254
|
+
payload.shortcut_target_file = Some(file_id);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
payload.files.push(entry);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
Ok(())
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
fn create_msi_cabinet(payload: &MsiPayload) -> Result<Vec<u8>> {
|
|
1265
|
+
let mut builder = CabinetBuilder::new();
|
|
1266
|
+
{
|
|
1267
|
+
let folder = builder.add_folder(CabCompressionType::MsZip);
|
|
1268
|
+
for file in &payload.files {
|
|
1269
|
+
folder.add_file(&file.cabinet_name);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
let cursor = Cursor::new(Vec::new());
|
|
1274
|
+
let mut cabinet = builder
|
|
1275
|
+
.build(cursor)
|
|
1276
|
+
.context("Could not start MSI cabinet")?;
|
|
1277
|
+
for file in &payload.files {
|
|
1278
|
+
let mut writer = cabinet
|
|
1279
|
+
.next_file()
|
|
1280
|
+
.context("Could not open next MSI cabinet file")?
|
|
1281
|
+
.context("MSI cabinet writer finished before all files were written")?;
|
|
1282
|
+
anyhow::ensure!(
|
|
1283
|
+
writer.file_name() == file.cabinet_name,
|
|
1284
|
+
"MSI cabinet file order drifted while writing {}",
|
|
1285
|
+
file.source.display()
|
|
1286
|
+
);
|
|
1287
|
+
let mut source = File::open(&file.source)
|
|
1288
|
+
.with_context(|| format!("Could not open {}", file.source.display()))?;
|
|
1289
|
+
io::copy(&mut source, &mut writer)
|
|
1290
|
+
.with_context(|| format!("Could not add {} to MSI cabinet", file.source.display()))?;
|
|
1291
|
+
}
|
|
1292
|
+
let cursor = cabinet.finish().context("Could not finish MSI cabinet")?;
|
|
1293
|
+
Ok(cursor.into_inner())
|
|
1294
|
+
}
|
|
1295
|
+
|
|
658
1296
|
fn debian_control_file(
|
|
659
1297
|
package: &PackageReport,
|
|
660
1298
|
deb_package: &str,
|
|
@@ -1024,6 +1662,51 @@ fn rpm_version(version: Option<&str>) -> String {
|
|
|
1024
1662
|
}
|
|
1025
1663
|
}
|
|
1026
1664
|
|
|
1665
|
+
fn windows_artifact_version(version: Option<&str>) -> String {
|
|
1666
|
+
let version = version.unwrap_or("0.1.0");
|
|
1667
|
+
let sanitized = version
|
|
1668
|
+
.chars()
|
|
1669
|
+
.map(|char| {
|
|
1670
|
+
if char.is_ascii_alphanumeric() || matches!(char, '.' | '-' | '_') {
|
|
1671
|
+
char
|
|
1672
|
+
} else {
|
|
1673
|
+
'-'
|
|
1674
|
+
}
|
|
1675
|
+
})
|
|
1676
|
+
.collect::<String>()
|
|
1677
|
+
.trim_matches(['-', '.', '_'])
|
|
1678
|
+
.to_string();
|
|
1679
|
+
|
|
1680
|
+
if sanitized.is_empty() {
|
|
1681
|
+
"0.1.0".to_string()
|
|
1682
|
+
} else {
|
|
1683
|
+
sanitized
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
fn msi_product_version(version: Option<&str>) -> String {
|
|
1688
|
+
let mut numbers = version
|
|
1689
|
+
.unwrap_or("0.1.0")
|
|
1690
|
+
.split(|char: char| !char.is_ascii_digit())
|
|
1691
|
+
.filter(|part| !part.is_empty())
|
|
1692
|
+
.filter_map(|part| part.parse::<u32>().ok())
|
|
1693
|
+
.take(3)
|
|
1694
|
+
.collect::<Vec<_>>();
|
|
1695
|
+
while numbers.len() < 3 {
|
|
1696
|
+
numbers.push(0);
|
|
1697
|
+
}
|
|
1698
|
+
if numbers.iter().all(|number| *number == 0) {
|
|
1699
|
+
numbers = vec![0, 1, 0];
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
format!(
|
|
1703
|
+
"{}.{}.{}",
|
|
1704
|
+
numbers[0].min(255),
|
|
1705
|
+
numbers[1].min(255),
|
|
1706
|
+
numbers[2].min(65_535)
|
|
1707
|
+
)
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1027
1710
|
fn debian_arch(arch: &str) -> String {
|
|
1028
1711
|
match arch {
|
|
1029
1712
|
"x64" => "amd64".to_string(),
|
|
@@ -1043,6 +1726,99 @@ fn rpm_arch(arch: &str) -> String {
|
|
|
1043
1726
|
}
|
|
1044
1727
|
}
|
|
1045
1728
|
|
|
1729
|
+
fn windows_arch(arch: &str) -> String {
|
|
1730
|
+
match arch {
|
|
1731
|
+
"ia32" => "x86".to_string(),
|
|
1732
|
+
arch => arch.to_string(),
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
fn msi_summary_arch(arch: &str) -> &'static str {
|
|
1737
|
+
match arch {
|
|
1738
|
+
"x64" => "x64",
|
|
1739
|
+
"arm64" => "Arm64",
|
|
1740
|
+
_ => "Intel",
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
fn msi_program_files_directory(arch: &str) -> &'static str {
|
|
1745
|
+
match arch {
|
|
1746
|
+
"x64" | "arm64" => "ProgramFiles64Folder",
|
|
1747
|
+
_ => "ProgramFilesFolder",
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
fn msi_component_attributes(arch: &str) -> i32 {
|
|
1752
|
+
match arch {
|
|
1753
|
+
"x64" | "arm64" => 256,
|
|
1754
|
+
_ => 0,
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
fn msi_filename(id: &str, long_name: &str) -> String {
|
|
1759
|
+
if is_msi_short_name(long_name) {
|
|
1760
|
+
return long_name.to_string();
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
let extension = Path::new(long_name)
|
|
1764
|
+
.extension()
|
|
1765
|
+
.and_then(|extension| extension.to_str())
|
|
1766
|
+
.map(|extension| {
|
|
1767
|
+
extension
|
|
1768
|
+
.chars()
|
|
1769
|
+
.filter(|char| char.is_ascii_alphanumeric())
|
|
1770
|
+
.take(3)
|
|
1771
|
+
.collect::<String>()
|
|
1772
|
+
})
|
|
1773
|
+
.filter(|extension| !extension.is_empty());
|
|
1774
|
+
let stem = id
|
|
1775
|
+
.chars()
|
|
1776
|
+
.filter(|char| char.is_ascii_alphanumeric())
|
|
1777
|
+
.take(8)
|
|
1778
|
+
.collect::<String>();
|
|
1779
|
+
let short = match extension {
|
|
1780
|
+
Some(extension) => format!("{stem}.{extension}"),
|
|
1781
|
+
None => stem,
|
|
1782
|
+
};
|
|
1783
|
+
format!("{short}|{long_name}")
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
fn is_msi_short_name(name: &str) -> bool {
|
|
1787
|
+
let Some(file_name) = Path::new(name).file_name().and_then(|name| name.to_str()) else {
|
|
1788
|
+
return false;
|
|
1789
|
+
};
|
|
1790
|
+
if file_name != name || file_name.is_empty() || file_name.contains(' ') {
|
|
1791
|
+
return false;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
let mut parts = file_name.split('.');
|
|
1795
|
+
let stem = parts.next().unwrap_or_default();
|
|
1796
|
+
let extension = parts.next();
|
|
1797
|
+
if parts.next().is_some() || stem.is_empty() || stem.len() > 8 {
|
|
1798
|
+
return false;
|
|
1799
|
+
}
|
|
1800
|
+
if extension.is_some_and(|extension| extension.is_empty() || extension.len() > 3) {
|
|
1801
|
+
return false;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
file_name
|
|
1805
|
+
.chars()
|
|
1806
|
+
.all(|char| char.is_ascii_alphanumeric() || matches!(char, '_' | '$' | '~' | '!' | '#'))
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
fn deterministic_guid(kind: &str, parts: &[&str]) -> Uuid {
|
|
1810
|
+
let key = format!("electron-cli:{kind}:{}", parts.join(":"));
|
|
1811
|
+
Uuid::new_v5(&Uuid::NAMESPACE_URL, key.as_bytes())
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
fn msi_guid(uuid: Uuid) -> String {
|
|
1815
|
+
format!("{{{}}}", uuid.hyphenated()).to_ascii_uppercase()
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
fn s(value: impl Into<String>) -> Value {
|
|
1819
|
+
Value::from(value.into())
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1046
1822
|
fn single_line(value: &str) -> String {
|
|
1047
1823
|
value
|
|
1048
1824
|
.chars()
|
|
@@ -1259,6 +2035,40 @@ mod tests {
|
|
|
1259
2035
|
let _ = fs::remove_dir_all(root);
|
|
1260
2036
|
}
|
|
1261
2037
|
|
|
2038
|
+
#[test]
|
|
2039
|
+
fn builds_make_report_for_msi_target() {
|
|
2040
|
+
let root = unique_temp_dir("msi-plan");
|
|
2041
|
+
write_package_json(&root);
|
|
2042
|
+
write_app_file(&root);
|
|
2043
|
+
write_fake_electron_dist(&root);
|
|
2044
|
+
|
|
2045
|
+
let args = MakeArgs {
|
|
2046
|
+
cwd: root.clone(),
|
|
2047
|
+
out_dir: PathBuf::from("out"),
|
|
2048
|
+
name: None,
|
|
2049
|
+
platform: Some("win32".to_string()),
|
|
2050
|
+
arch: Some("x64".to_string()),
|
|
2051
|
+
target: crate::cli::MakeTarget::Msi,
|
|
2052
|
+
skip_package: false,
|
|
2053
|
+
force: false,
|
|
2054
|
+
dry_run: true,
|
|
2055
|
+
json: true,
|
|
2056
|
+
};
|
|
2057
|
+
let report = build_report(&args).expect("report should build");
|
|
2058
|
+
|
|
2059
|
+
assert_eq!(report.target, "msi");
|
|
2060
|
+
assert!(Path::new(report.artifact.as_str()).ends_with(
|
|
2061
|
+
PathBuf::from("out")
|
|
2062
|
+
.join("make")
|
|
2063
|
+
.join("msi")
|
|
2064
|
+
.join("win32")
|
|
2065
|
+
.join("x64")
|
|
2066
|
+
.join("starter-app-0.1.0-x64.msi")
|
|
2067
|
+
));
|
|
2068
|
+
|
|
2069
|
+
let _ = fs::remove_dir_all(root);
|
|
2070
|
+
}
|
|
2071
|
+
|
|
1262
2072
|
#[test]
|
|
1263
2073
|
fn makes_zip_artifact_after_packaging() {
|
|
1264
2074
|
let root = unique_temp_dir("execute");
|
|
@@ -1487,6 +2297,76 @@ mod tests {
|
|
|
1487
2297
|
let _ = fs::remove_dir_all(root);
|
|
1488
2298
|
}
|
|
1489
2299
|
|
|
2300
|
+
#[test]
|
|
2301
|
+
fn writes_msi_archive_with_database_tables_and_embedded_cabinet() {
|
|
2302
|
+
let root = unique_temp_dir("msi-archive");
|
|
2303
|
+
write_package_json(&root);
|
|
2304
|
+
write_app_file(&root);
|
|
2305
|
+
write_fake_electron_dist(&root);
|
|
2306
|
+
|
|
2307
|
+
let args = MakeArgs {
|
|
2308
|
+
cwd: root.clone(),
|
|
2309
|
+
out_dir: PathBuf::from("out"),
|
|
2310
|
+
name: None,
|
|
2311
|
+
platform: Some("win32".to_string()),
|
|
2312
|
+
arch: Some("x64".to_string()),
|
|
2313
|
+
target: crate::cli::MakeTarget::Msi,
|
|
2314
|
+
skip_package: false,
|
|
2315
|
+
force: false,
|
|
2316
|
+
dry_run: true,
|
|
2317
|
+
json: true,
|
|
2318
|
+
};
|
|
2319
|
+
let report = build_report(&args).expect("report should build");
|
|
2320
|
+
write_fake_windows_bundle(
|
|
2321
|
+
Path::new(report.package.bundle_dir().as_str()),
|
|
2322
|
+
"starter-app.exe",
|
|
2323
|
+
);
|
|
2324
|
+
|
|
2325
|
+
write_msi_archive(&report.package, Path::new(report.artifact.as_str()))
|
|
2326
|
+
.expect("msi should be written");
|
|
2327
|
+
|
|
2328
|
+
let mut installer = msi::open(report.artifact.as_str()).expect("msi should parse");
|
|
2329
|
+
assert_eq!(installer.summary_info().arch(), Some("x64"));
|
|
2330
|
+
assert!(installer.has_table("Property"));
|
|
2331
|
+
assert!(installer.has_table("Directory"));
|
|
2332
|
+
assert!(installer.has_table("File"));
|
|
2333
|
+
assert!(installer.has_table("Media"));
|
|
2334
|
+
assert!(installer.has_stream("app.cab"));
|
|
2335
|
+
|
|
2336
|
+
let properties = msi_rows(&mut installer, "Property");
|
|
2337
|
+
assert!(properties.contains(&vec![
|
|
2338
|
+
Value::from("ProductName"),
|
|
2339
|
+
Value::from("starter-app")
|
|
2340
|
+
]));
|
|
2341
|
+
assert!(properties.contains(&vec![Value::from("ProductVersion"), Value::from("0.1.0")]));
|
|
2342
|
+
|
|
2343
|
+
let files = msi_rows(&mut installer, "File");
|
|
2344
|
+
assert!(files
|
|
2345
|
+
.iter()
|
|
2346
|
+
.any(|row| row[2] == Value::from("F0001.jso|package.json")));
|
|
2347
|
+
assert!(files
|
|
2348
|
+
.iter()
|
|
2349
|
+
.any(|row| row[2] == Value::from("F0002.exe|starter-app.exe")));
|
|
2350
|
+
|
|
2351
|
+
let mut cabinet_bytes = Vec::new();
|
|
2352
|
+
installer
|
|
2353
|
+
.read_stream("app.cab")
|
|
2354
|
+
.expect("cab stream should open")
|
|
2355
|
+
.read_to_end(&mut cabinet_bytes)
|
|
2356
|
+
.expect("cab stream should read");
|
|
2357
|
+
let mut cabinet =
|
|
2358
|
+
cab::Cabinet::new(Cursor::new(cabinet_bytes)).expect("cabinet should parse");
|
|
2359
|
+
let mut package_json = String::new();
|
|
2360
|
+
cabinet
|
|
2361
|
+
.read_file("F0001")
|
|
2362
|
+
.expect("package.json cabinet entry should open")
|
|
2363
|
+
.read_to_string(&mut package_json)
|
|
2364
|
+
.expect("package.json cabinet entry should read");
|
|
2365
|
+
assert_eq!(package_json, "{}");
|
|
2366
|
+
|
|
2367
|
+
let _ = fs::remove_dir_all(root);
|
|
2368
|
+
}
|
|
2369
|
+
|
|
1490
2370
|
#[test]
|
|
1491
2371
|
fn makes_deb_artifact_after_packaging_on_linux() {
|
|
1492
2372
|
if !cfg!(target_os = "linux") {
|
|
@@ -1613,6 +2493,15 @@ mod tests {
|
|
|
1613
2493
|
.expect("fake app package should be written");
|
|
1614
2494
|
}
|
|
1615
2495
|
|
|
2496
|
+
fn write_fake_windows_bundle(bundle_dir: &Path, executable_name: &str) {
|
|
2497
|
+
fs::create_dir_all(bundle_dir.join("resources/app"))
|
|
2498
|
+
.expect("fake Windows resources should be created");
|
|
2499
|
+
fs::write(bundle_dir.join(executable_name), "fake exe")
|
|
2500
|
+
.expect("fake Windows executable should be written");
|
|
2501
|
+
fs::write(bundle_dir.join("resources/app/package.json"), "{}")
|
|
2502
|
+
.expect("fake app package should be written");
|
|
2503
|
+
}
|
|
2504
|
+
|
|
1616
2505
|
fn write_fake_electron_dist(root: &Path) {
|
|
1617
2506
|
let dist = root.join("node_modules/electron/dist");
|
|
1618
2507
|
if cfg!(target_os = "macos") {
|
|
@@ -1707,4 +2596,12 @@ mod tests {
|
|
|
1707
2596
|
== path
|
|
1708
2597
|
})
|
|
1709
2598
|
}
|
|
2599
|
+
|
|
2600
|
+
fn msi_rows(installer: &mut msi::Package<File>, table: &str) -> Vec<Vec<Value>> {
|
|
2601
|
+
installer
|
|
2602
|
+
.select_rows(msi::Select::table(table))
|
|
2603
|
+
.expect("msi rows should select")
|
|
2604
|
+
.map(|row| (0..row.len()).map(|index| row[index].clone()).collect())
|
|
2605
|
+
.collect()
|
|
2606
|
+
}
|
|
1710
2607
|
}
|