electron-cli 0.3.0-alpha.7 → 0.3.0-alpha.8
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 +76 -1
- package/Cargo.toml +4 -2
- package/README.md +4 -3
- package/package.json +1 -1
- package/src/cli.rs +4 -2
- package/src/commands/make.rs +607 -9
- package/src/commands/package.rs +4 -0
package/Cargo.lock
CHANGED
|
@@ -70,6 +70,12 @@ version = "0.22.1"
|
|
|
70
70
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
71
71
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
|
72
72
|
|
|
73
|
+
[[package]]
|
|
74
|
+
name = "bitflags"
|
|
75
|
+
version = "2.11.1"
|
|
76
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
77
|
+
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
|
78
|
+
|
|
73
79
|
[[package]]
|
|
74
80
|
name = "camino"
|
|
75
81
|
version = "1.2.2"
|
|
@@ -151,14 +157,16 @@ dependencies = [
|
|
|
151
157
|
|
|
152
158
|
[[package]]
|
|
153
159
|
name = "electron-cli"
|
|
154
|
-
version = "0.3.0-alpha.
|
|
160
|
+
version = "0.3.0-alpha.8"
|
|
155
161
|
dependencies = [
|
|
156
162
|
"anyhow",
|
|
157
163
|
"camino",
|
|
158
164
|
"clap",
|
|
165
|
+
"flate2",
|
|
159
166
|
"plist",
|
|
160
167
|
"serde",
|
|
161
168
|
"serde_json",
|
|
169
|
+
"tar",
|
|
162
170
|
"zip",
|
|
163
171
|
]
|
|
164
172
|
|
|
@@ -168,12 +176,33 @@ version = "1.0.2"
|
|
|
168
176
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
169
177
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
|
170
178
|
|
|
179
|
+
[[package]]
|
|
180
|
+
name = "errno"
|
|
181
|
+
version = "0.3.14"
|
|
182
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
183
|
+
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
|
184
|
+
dependencies = [
|
|
185
|
+
"libc",
|
|
186
|
+
"windows-sys",
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
[[package]]
|
|
190
|
+
name = "filetime"
|
|
191
|
+
version = "0.2.29"
|
|
192
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
193
|
+
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
|
|
194
|
+
dependencies = [
|
|
195
|
+
"cfg-if",
|
|
196
|
+
"libc",
|
|
197
|
+
]
|
|
198
|
+
|
|
171
199
|
[[package]]
|
|
172
200
|
name = "flate2"
|
|
173
201
|
version = "1.1.9"
|
|
174
202
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
175
203
|
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
|
176
204
|
dependencies = [
|
|
205
|
+
"crc32fast",
|
|
177
206
|
"miniz_oxide",
|
|
178
207
|
"zlib-rs",
|
|
179
208
|
]
|
|
@@ -212,6 +241,18 @@ version = "1.0.18"
|
|
|
212
241
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
213
242
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
|
214
243
|
|
|
244
|
+
[[package]]
|
|
245
|
+
name = "libc"
|
|
246
|
+
version = "0.2.186"
|
|
247
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
248
|
+
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
|
249
|
+
|
|
250
|
+
[[package]]
|
|
251
|
+
name = "linux-raw-sys"
|
|
252
|
+
version = "0.12.1"
|
|
253
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
254
|
+
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
|
255
|
+
|
|
215
256
|
[[package]]
|
|
216
257
|
name = "memchr"
|
|
217
258
|
version = "2.8.1"
|
|
@@ -286,6 +327,19 @@ dependencies = [
|
|
|
286
327
|
"proc-macro2",
|
|
287
328
|
]
|
|
288
329
|
|
|
330
|
+
[[package]]
|
|
331
|
+
name = "rustix"
|
|
332
|
+
version = "1.1.4"
|
|
333
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
334
|
+
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
|
335
|
+
dependencies = [
|
|
336
|
+
"bitflags",
|
|
337
|
+
"errno",
|
|
338
|
+
"libc",
|
|
339
|
+
"linux-raw-sys",
|
|
340
|
+
"windows-sys",
|
|
341
|
+
]
|
|
342
|
+
|
|
289
343
|
[[package]]
|
|
290
344
|
name = "serde"
|
|
291
345
|
version = "1.0.228"
|
|
@@ -352,6 +406,17 @@ dependencies = [
|
|
|
352
406
|
"unicode-ident",
|
|
353
407
|
]
|
|
354
408
|
|
|
409
|
+
[[package]]
|
|
410
|
+
name = "tar"
|
|
411
|
+
version = "0.4.46"
|
|
412
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
413
|
+
checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
|
|
414
|
+
dependencies = [
|
|
415
|
+
"filetime",
|
|
416
|
+
"libc",
|
|
417
|
+
"xattr",
|
|
418
|
+
]
|
|
419
|
+
|
|
355
420
|
[[package]]
|
|
356
421
|
name = "time"
|
|
357
422
|
version = "0.3.47"
|
|
@@ -416,6 +481,16 @@ dependencies = [
|
|
|
416
481
|
"windows-link",
|
|
417
482
|
]
|
|
418
483
|
|
|
484
|
+
[[package]]
|
|
485
|
+
name = "xattr"
|
|
486
|
+
version = "1.6.1"
|
|
487
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
488
|
+
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
|
|
489
|
+
dependencies = [
|
|
490
|
+
"libc",
|
|
491
|
+
"rustix",
|
|
492
|
+
]
|
|
493
|
+
|
|
419
494
|
[[package]]
|
|
420
495
|
name = "zip"
|
|
421
496
|
version = "8.6.0"
|
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.8"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
description = "Experimental Rust CLI for Electron project diagnostics and workflow automation"
|
|
6
6
|
license = "MIT"
|
|
@@ -10,7 +10,9 @@ repository = "https://github.com/Ikana/electron-cli"
|
|
|
10
10
|
anyhow = "1.0"
|
|
11
11
|
camino = { version = "1.1", features = ["serde1"] }
|
|
12
12
|
clap = { version = "4.6", features = ["derive"] }
|
|
13
|
+
flate2 = { version = "1.1", default-features = false, features = ["rust_backend"] }
|
|
14
|
+
plist = "1"
|
|
13
15
|
serde = { version = "1.0", features = ["derive"] }
|
|
14
16
|
serde_json = "1.0"
|
|
15
|
-
|
|
17
|
+
tar = "0.4"
|
|
16
18
|
zip = { version = "8.6.0", default-features = false, features = ["deflate-flate2-zlib-rs"] }
|
package/README.md
CHANGED
|
@@ -42,10 +42,10 @@ The Rust-native flow currently owns:
|
|
|
42
42
|
- `init --template minimal`: writes a local Electron starter without Electron Forge.
|
|
43
43
|
- `start`: launches the installed Electron runtime directly.
|
|
44
44
|
- `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.
|
|
45
|
-
- `make`: runs `package` and writes a
|
|
45
|
+
- `make`: runs `package` and writes a distributable under `out/make/<target>/<platform>/<arch>/`; ZIP works on all platforms, and `--target deb` writes a Linux Debian package.
|
|
46
46
|
- `publish`: runs `make` and publishes the distributable to a local directory with a manifest.
|
|
47
47
|
|
|
48
|
-
Remote publishers such as GitHub Releases are not implemented yet.
|
|
48
|
+
Remote publishers such as GitHub Releases are not implemented yet. DMG, RPM, Windows installers, Windows/Linux icon embedding, signing, and notarization are also still TODO.
|
|
49
49
|
|
|
50
50
|
Package metadata can be configured in `package.json`:
|
|
51
51
|
|
|
@@ -98,6 +98,7 @@ cargo run -- init my-app
|
|
|
98
98
|
cargo run -- start --dry-run
|
|
99
99
|
cargo run -- package --dry-run
|
|
100
100
|
cargo run -- make --dry-run
|
|
101
|
+
cargo run -- make --target deb --dry-run
|
|
101
102
|
cargo run -- publish --dry-run
|
|
102
103
|
```
|
|
103
104
|
|
|
@@ -122,7 +123,7 @@ The inspection and planning commands support `--json` so agents and scripts can
|
|
|
122
123
|
`init --dry-run --json` shows whether the CLI will write native template files or delegate to `create-electron-app`.
|
|
123
124
|
`start --dry-run --json` shows the Electron executable that will be launched.
|
|
124
125
|
`package --dry-run --json` shows the runtime, app file, metadata, icon, and extra-resource copy plan.
|
|
125
|
-
`make --dry-run --json` shows the package prerequisite and
|
|
126
|
+
`make --dry-run --json` shows the package prerequisite and selected maker artifact path.
|
|
126
127
|
`publish --dry-run --json` shows the make prerequisite, destination artifact, and manifest path.
|
|
127
128
|
|
|
128
129
|
```sh
|
package/package.json
CHANGED
package/src/cli.rs
CHANGED
|
@@ -240,7 +240,7 @@ pub struct PublishArgs {
|
|
|
240
240
|
pub json: bool,
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
#[derive(Debug, Clone, Copy, ValueEnum)]
|
|
243
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
|
|
244
244
|
#[value(rename_all = "lower")]
|
|
245
245
|
pub enum PackageManager {
|
|
246
246
|
Npm,
|
|
@@ -260,15 +260,17 @@ impl PackageManager {
|
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
#[derive(Debug, Clone, Copy, ValueEnum)]
|
|
263
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
|
|
264
264
|
#[value(rename_all = "lower")]
|
|
265
265
|
pub enum MakeTarget {
|
|
266
|
+
Deb,
|
|
266
267
|
Zip,
|
|
267
268
|
}
|
|
268
269
|
|
|
269
270
|
impl MakeTarget {
|
|
270
271
|
pub fn as_str(self) -> &'static str {
|
|
271
272
|
match self {
|
|
273
|
+
MakeTarget::Deb => "deb",
|
|
272
274
|
MakeTarget::Zip => "zip",
|
|
273
275
|
}
|
|
274
276
|
}
|
package/src/commands/make.rs
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
use std::{
|
|
2
2
|
fs,
|
|
3
3
|
fs::File,
|
|
4
|
-
io::{self, BufWriter},
|
|
4
|
+
io::{self, BufWriter, Write},
|
|
5
5
|
path::{Path, PathBuf},
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
use anyhow::{bail, Context, Result};
|
|
9
9
|
use camino::Utf8PathBuf;
|
|
10
|
+
use flate2::{write::GzEncoder, Compression};
|
|
10
11
|
use serde::Serialize;
|
|
12
|
+
use tar::{Builder as TarBuilder, Header as TarHeader};
|
|
11
13
|
use zip::{write::SimpleFileOptions, CompressionMethod, ZipWriter};
|
|
12
14
|
|
|
13
15
|
use crate::{
|
|
14
|
-
cli::{MakeArgs, PackageArgs},
|
|
16
|
+
cli::{MakeArgs, MakeTarget, PackageArgs},
|
|
15
17
|
commands::package::{self, PackageReport},
|
|
16
18
|
output,
|
|
17
19
|
};
|
|
@@ -67,14 +69,15 @@ pub(crate) fn build_report(args: &MakeArgs) -> Result<MakeReport> {
|
|
|
67
69
|
.join(args.target.as_str())
|
|
68
70
|
.join(package.platform())
|
|
69
71
|
.join(package.arch());
|
|
70
|
-
let artifact = make_dir.
|
|
71
|
-
"{}-{}-{}.zip",
|
|
72
|
-
package.artifact_stem(),
|
|
73
|
-
package.platform(),
|
|
74
|
-
package.arch()
|
|
75
|
-
));
|
|
72
|
+
let artifact = make_artifact_path(&make_dir, &package, args.target);
|
|
76
73
|
|
|
77
74
|
let mut warnings = package.warnings().to_vec();
|
|
75
|
+
if args.target == MakeTarget::Deb && package.platform() != "linux" {
|
|
76
|
+
warnings.push(format!(
|
|
77
|
+
"Deb maker only supports linux packages; target platform is {}.",
|
|
78
|
+
package.platform()
|
|
79
|
+
));
|
|
80
|
+
}
|
|
78
81
|
if args.skip_package && !Path::new(package.bundle_dir().as_str()).exists() {
|
|
79
82
|
warnings.push(format!(
|
|
80
83
|
"Package output does not exist: {}.",
|
|
@@ -128,7 +131,12 @@ pub(crate) fn execute_make(report: &mut MakeReport, args: &MakeArgs) -> Result<(
|
|
|
128
131
|
|
|
129
132
|
fs::create_dir_all(report.make_dir.as_str())
|
|
130
133
|
.with_context(|| format!("Could not create {}", report.make_dir))?;
|
|
131
|
-
|
|
134
|
+
match args.target {
|
|
135
|
+
MakeTarget::Zip => {
|
|
136
|
+
write_zip_archive(Path::new(report.package.bundle_dir().as_str()), artifact)?
|
|
137
|
+
}
|
|
138
|
+
MakeTarget::Deb => write_deb_archive(&report.package, artifact)?,
|
|
139
|
+
}
|
|
132
140
|
|
|
133
141
|
Ok(())
|
|
134
142
|
}
|
|
@@ -173,6 +181,23 @@ fn print_report(report: &MakeReport, json: bool) -> Result<()> {
|
|
|
173
181
|
Ok(())
|
|
174
182
|
}
|
|
175
183
|
|
|
184
|
+
fn make_artifact_path(make_dir: &Path, package: &PackageReport, target: MakeTarget) -> PathBuf {
|
|
185
|
+
match target {
|
|
186
|
+
MakeTarget::Zip => make_dir.join(format!(
|
|
187
|
+
"{}-{}-{}.zip",
|
|
188
|
+
package.artifact_stem(),
|
|
189
|
+
package.platform(),
|
|
190
|
+
package.arch()
|
|
191
|
+
)),
|
|
192
|
+
MakeTarget::Deb => make_dir.join(format!(
|
|
193
|
+
"{}_{}_{}.deb",
|
|
194
|
+
debian_package_name(&package.artifact_stem()),
|
|
195
|
+
debian_version(package.project().version.as_deref()),
|
|
196
|
+
debian_arch(package.arch())
|
|
197
|
+
)),
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
176
201
|
fn write_zip_archive(source: &Path, artifact: &Path) -> Result<()> {
|
|
177
202
|
if !source.exists() {
|
|
178
203
|
bail!("Package output does not exist: {}", source.display());
|
|
@@ -263,6 +288,386 @@ fn directory_options(metadata: &fs::Metadata) -> SimpleFileOptions {
|
|
|
263
288
|
.unix_permissions(unix_mode(metadata, 0o755))
|
|
264
289
|
}
|
|
265
290
|
|
|
291
|
+
fn write_deb_archive(package: &PackageReport, artifact: &Path) -> Result<()> {
|
|
292
|
+
if package.platform() != "linux" {
|
|
293
|
+
bail!(
|
|
294
|
+
"Deb maker only supports linux packages. Requested {}.",
|
|
295
|
+
package.platform()
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let source = Path::new(package.bundle_dir().as_str());
|
|
300
|
+
if !source.exists() {
|
|
301
|
+
bail!("Package output does not exist: {}", source.display());
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
let parent = artifact
|
|
305
|
+
.parent()
|
|
306
|
+
.with_context(|| format!("Artifact path has no parent: {}", artifact.display()))?;
|
|
307
|
+
fs::create_dir_all(parent).with_context(|| format!("Could not create {}", parent.display()))?;
|
|
308
|
+
|
|
309
|
+
let deb_package = debian_package_name(&package.artifact_stem());
|
|
310
|
+
let version = debian_version(package.project().version.as_deref());
|
|
311
|
+
let arch = debian_arch(package.arch());
|
|
312
|
+
let installed_size = directory_size(source)?.div_ceil(1024).max(1);
|
|
313
|
+
let control = debian_control_file(package, &deb_package, &version, &arch, installed_size);
|
|
314
|
+
let control_tar =
|
|
315
|
+
gzip_tar(|builder| append_bytes_to_tar(builder, "./control", control.as_bytes(), 0o644))?;
|
|
316
|
+
let data_tar = gzip_tar(|builder| append_deb_data_tar(builder, package, source, &deb_package))?;
|
|
317
|
+
|
|
318
|
+
write_ar_archive(
|
|
319
|
+
artifact,
|
|
320
|
+
&[
|
|
321
|
+
ArMember {
|
|
322
|
+
name: "debian-binary",
|
|
323
|
+
mode: 0o100644,
|
|
324
|
+
data: b"2.0\n".to_vec(),
|
|
325
|
+
},
|
|
326
|
+
ArMember {
|
|
327
|
+
name: "control.tar.gz",
|
|
328
|
+
mode: 0o100644,
|
|
329
|
+
data: control_tar,
|
|
330
|
+
},
|
|
331
|
+
ArMember {
|
|
332
|
+
name: "data.tar.gz",
|
|
333
|
+
mode: 0o100644,
|
|
334
|
+
data: data_tar,
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
fn debian_control_file(
|
|
341
|
+
package: &PackageReport,
|
|
342
|
+
deb_package: &str,
|
|
343
|
+
version: &str,
|
|
344
|
+
arch: &str,
|
|
345
|
+
installed_size: u64,
|
|
346
|
+
) -> String {
|
|
347
|
+
format!(
|
|
348
|
+
"Package: {deb_package}\n\
|
|
349
|
+
Version: {version}\n\
|
|
350
|
+
Section: utils\n\
|
|
351
|
+
Priority: optional\n\
|
|
352
|
+
Architecture: {arch}\n\
|
|
353
|
+
Maintainer: electron-cli <noreply@example.invalid>\n\
|
|
354
|
+
Installed-Size: {installed_size}\n\
|
|
355
|
+
Description: {description}\n\
|
|
356
|
+
Electron application packaged by electron-cli.\n",
|
|
357
|
+
description = single_line(package.app_name())
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
fn append_deb_data_tar(
|
|
362
|
+
builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
|
|
363
|
+
package: &PackageReport,
|
|
364
|
+
source: &Path,
|
|
365
|
+
deb_package: &str,
|
|
366
|
+
) -> Result<()> {
|
|
367
|
+
for directory in [
|
|
368
|
+
"./",
|
|
369
|
+
"./opt",
|
|
370
|
+
"./usr",
|
|
371
|
+
"./usr/bin",
|
|
372
|
+
"./usr/share",
|
|
373
|
+
"./usr/share/applications",
|
|
374
|
+
] {
|
|
375
|
+
append_directory_to_tar(builder, directory, 0o755)?;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
let app_root = format!("./opt/{deb_package}");
|
|
379
|
+
append_directory_to_tar(builder, &app_root, 0o755)?;
|
|
380
|
+
append_directory_contents_to_tar(builder, source, Path::new(&app_root))?;
|
|
381
|
+
|
|
382
|
+
let executable = format!("/opt/{deb_package}/{}", package.executable_name());
|
|
383
|
+
append_symlink_to_tar(
|
|
384
|
+
builder,
|
|
385
|
+
format!("./usr/bin/{deb_package}"),
|
|
386
|
+
&executable,
|
|
387
|
+
0o777,
|
|
388
|
+
)?;
|
|
389
|
+
append_bytes_to_tar(
|
|
390
|
+
builder,
|
|
391
|
+
format!("./usr/share/applications/{deb_package}.desktop"),
|
|
392
|
+
debian_desktop_file(package, deb_package, &executable).as_bytes(),
|
|
393
|
+
0o644,
|
|
394
|
+
)?;
|
|
395
|
+
|
|
396
|
+
Ok(())
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
fn debian_desktop_file(package: &PackageReport, deb_package: &str, executable: &str) -> String {
|
|
400
|
+
format!(
|
|
401
|
+
"[Desktop Entry]\n\
|
|
402
|
+
Name={name}\n\
|
|
403
|
+
Exec={executable} %U\n\
|
|
404
|
+
Terminal=false\n\
|
|
405
|
+
Type=Application\n\
|
|
406
|
+
StartupWMClass={wm_class}\n\
|
|
407
|
+
Categories=Utility;\n",
|
|
408
|
+
name = single_line(package.app_name()),
|
|
409
|
+
wm_class = deb_package
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
fn gzip_tar(
|
|
414
|
+
write_contents: impl FnOnce(&mut TarBuilder<GzEncoder<Vec<u8>>>) -> Result<()>,
|
|
415
|
+
) -> Result<Vec<u8>> {
|
|
416
|
+
let encoder = GzEncoder::new(Vec::new(), Compression::default());
|
|
417
|
+
let mut builder = TarBuilder::new(encoder);
|
|
418
|
+
builder.mode(tar::HeaderMode::Deterministic);
|
|
419
|
+
write_contents(&mut builder)?;
|
|
420
|
+
builder.finish().context("Could not finish tar archive")?;
|
|
421
|
+
let encoder = builder
|
|
422
|
+
.into_inner()
|
|
423
|
+
.context("Could not retrieve gzip encoder")?;
|
|
424
|
+
encoder.finish().context("Could not finish gzip archive")
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
fn append_directory_contents_to_tar(
|
|
428
|
+
builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
|
|
429
|
+
source: &Path,
|
|
430
|
+
destination: &Path,
|
|
431
|
+
) -> Result<()> {
|
|
432
|
+
let mut entries = fs::read_dir(source)
|
|
433
|
+
.with_context(|| format!("Could not read {}", source.display()))?
|
|
434
|
+
.collect::<Result<Vec<_>, io::Error>>()?;
|
|
435
|
+
entries.sort_by_key(|entry| entry.path());
|
|
436
|
+
|
|
437
|
+
for entry in entries {
|
|
438
|
+
let source_path = entry.path();
|
|
439
|
+
let destination_path = destination.join(entry.file_name());
|
|
440
|
+
append_path_to_tar(builder, &source_path, &destination_path)?;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
Ok(())
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
fn append_path_to_tar(
|
|
447
|
+
builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
|
|
448
|
+
source: &Path,
|
|
449
|
+
destination: &Path,
|
|
450
|
+
) -> Result<()> {
|
|
451
|
+
let metadata = fs::symlink_metadata(source)
|
|
452
|
+
.with_context(|| format!("Could not stat {}", source.display()))?;
|
|
453
|
+
|
|
454
|
+
if metadata.is_dir() {
|
|
455
|
+
append_directory_to_tar(builder, destination, unix_mode(&metadata, 0o755))?;
|
|
456
|
+
append_directory_contents_to_tar(builder, source, destination)?;
|
|
457
|
+
} else if metadata.file_type().is_symlink() {
|
|
458
|
+
let target = fs::read_link(source)
|
|
459
|
+
.with_context(|| format!("Could not read link {}", source.display()))?;
|
|
460
|
+
append_symlink_to_tar(builder, destination, &target, 0o777)?;
|
|
461
|
+
} else if metadata.is_file() {
|
|
462
|
+
append_file_to_tar(builder, source, destination, &metadata)?;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
Ok(())
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
fn append_directory_to_tar(
|
|
469
|
+
builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
|
|
470
|
+
path: impl AsRef<Path>,
|
|
471
|
+
mode: u32,
|
|
472
|
+
) -> Result<()> {
|
|
473
|
+
let mut header = TarHeader::new_gnu();
|
|
474
|
+
header.set_entry_type(tar::EntryType::Directory);
|
|
475
|
+
header.set_size(0);
|
|
476
|
+
header.set_mode(mode);
|
|
477
|
+
header.set_mtime(0);
|
|
478
|
+
header.set_cksum();
|
|
479
|
+
builder
|
|
480
|
+
.append_data(&mut header, path.as_ref(), io::empty())
|
|
481
|
+
.with_context(|| format!("Could not add {} to data tar", path.as_ref().display()))
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
fn append_file_to_tar(
|
|
485
|
+
builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
|
|
486
|
+
source: &Path,
|
|
487
|
+
destination: &Path,
|
|
488
|
+
metadata: &fs::Metadata,
|
|
489
|
+
) -> Result<()> {
|
|
490
|
+
let mut header = TarHeader::new_gnu();
|
|
491
|
+
header.set_entry_type(tar::EntryType::Regular);
|
|
492
|
+
header.set_size(metadata.len());
|
|
493
|
+
header.set_mode(unix_mode(metadata, 0o644));
|
|
494
|
+
header.set_mtime(0);
|
|
495
|
+
header.set_cksum();
|
|
496
|
+
let mut file =
|
|
497
|
+
File::open(source).with_context(|| format!("Could not open {}", source.display()))?;
|
|
498
|
+
builder
|
|
499
|
+
.append_data(&mut header, destination, &mut file)
|
|
500
|
+
.with_context(|| format!("Could not add {} to data tar", source.display()))
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
fn append_bytes_to_tar(
|
|
504
|
+
builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
|
|
505
|
+
path: impl AsRef<Path>,
|
|
506
|
+
contents: &[u8],
|
|
507
|
+
mode: u32,
|
|
508
|
+
) -> Result<()> {
|
|
509
|
+
let mut header = TarHeader::new_gnu();
|
|
510
|
+
header.set_entry_type(tar::EntryType::Regular);
|
|
511
|
+
header.set_size(contents.len() as u64);
|
|
512
|
+
header.set_mode(mode);
|
|
513
|
+
header.set_mtime(0);
|
|
514
|
+
header.set_cksum();
|
|
515
|
+
builder
|
|
516
|
+
.append_data(&mut header, path.as_ref(), contents)
|
|
517
|
+
.with_context(|| format!("Could not add {} to tar", path.as_ref().display()))
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
fn append_symlink_to_tar(
|
|
521
|
+
builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
|
|
522
|
+
path: impl AsRef<Path>,
|
|
523
|
+
target: impl AsRef<Path>,
|
|
524
|
+
mode: u32,
|
|
525
|
+
) -> Result<()> {
|
|
526
|
+
let mut header = TarHeader::new_gnu();
|
|
527
|
+
header.set_entry_type(tar::EntryType::Symlink);
|
|
528
|
+
header.set_size(0);
|
|
529
|
+
header.set_mode(mode);
|
|
530
|
+
header.set_mtime(0);
|
|
531
|
+
header
|
|
532
|
+
.set_link_name(target.as_ref())
|
|
533
|
+
.with_context(|| format!("Could not set link target for {}", path.as_ref().display()))?;
|
|
534
|
+
header.set_cksum();
|
|
535
|
+
builder
|
|
536
|
+
.append_data(&mut header, path.as_ref(), io::empty())
|
|
537
|
+
.with_context(|| format!("Could not add {} to tar", path.as_ref().display()))
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
struct ArMember {
|
|
541
|
+
name: &'static str,
|
|
542
|
+
mode: u32,
|
|
543
|
+
data: Vec<u8>,
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
fn write_ar_archive(artifact: &Path, members: &[ArMember]) -> Result<()> {
|
|
547
|
+
let mut file = BufWriter::new(
|
|
548
|
+
File::create(artifact)
|
|
549
|
+
.with_context(|| format!("Could not create {}", artifact.display()))?,
|
|
550
|
+
);
|
|
551
|
+
file.write_all(b"!<arch>\n")
|
|
552
|
+
.with_context(|| format!("Could not write {}", artifact.display()))?;
|
|
553
|
+
|
|
554
|
+
for member in members {
|
|
555
|
+
write_ar_member(&mut file, member)
|
|
556
|
+
.with_context(|| format!("Could not add {} to {}", member.name, artifact.display()))?;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
file.flush()
|
|
560
|
+
.with_context(|| format!("Could not finish {}", artifact.display()))
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
fn write_ar_member(writer: &mut impl Write, member: &ArMember) -> Result<()> {
|
|
564
|
+
let name = format!("{}/", member.name);
|
|
565
|
+
if name.len() > 16 {
|
|
566
|
+
bail!("ar member name is too long: {}", member.name);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
let header = format!(
|
|
570
|
+
"{name:<16}{mtime:<12}{uid:<6}{gid:<6}{mode:<8o}{size:<10}`\n",
|
|
571
|
+
mtime = 0,
|
|
572
|
+
uid = 0,
|
|
573
|
+
gid = 0,
|
|
574
|
+
mode = member.mode,
|
|
575
|
+
size = member.data.len()
|
|
576
|
+
);
|
|
577
|
+
debug_assert_eq!(header.len(), 60);
|
|
578
|
+
writer.write_all(header.as_bytes())?;
|
|
579
|
+
writer.write_all(&member.data)?;
|
|
580
|
+
if member.data.len() % 2 == 1 {
|
|
581
|
+
writer.write_all(b"\n")?;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
Ok(())
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
fn directory_size(path: &Path) -> Result<u64> {
|
|
588
|
+
let metadata =
|
|
589
|
+
fs::symlink_metadata(path).with_context(|| format!("Could not stat {}", path.display()))?;
|
|
590
|
+
if metadata.is_file() {
|
|
591
|
+
return Ok(metadata.len());
|
|
592
|
+
}
|
|
593
|
+
if !metadata.is_dir() {
|
|
594
|
+
return Ok(0);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
let mut size = 0;
|
|
598
|
+
for entry in fs::read_dir(path).with_context(|| format!("Could not read {}", path.display()))? {
|
|
599
|
+
let entry = entry?;
|
|
600
|
+
size += directory_size(&entry.path())?;
|
|
601
|
+
}
|
|
602
|
+
Ok(size)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
fn debian_package_name(name: &str) -> String {
|
|
606
|
+
let mut package = name
|
|
607
|
+
.to_ascii_lowercase()
|
|
608
|
+
.chars()
|
|
609
|
+
.map(|char| {
|
|
610
|
+
if char.is_ascii_alphanumeric() || matches!(char, '+' | '-' | '.') {
|
|
611
|
+
char
|
|
612
|
+
} else {
|
|
613
|
+
'-'
|
|
614
|
+
}
|
|
615
|
+
})
|
|
616
|
+
.collect::<String>()
|
|
617
|
+
.trim_matches(['+', '-', '.'])
|
|
618
|
+
.to_string();
|
|
619
|
+
|
|
620
|
+
if package.len() < 2 {
|
|
621
|
+
package.push_str("app");
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
package
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
fn debian_version(version: Option<&str>) -> String {
|
|
628
|
+
let version = version.unwrap_or("0.1.0");
|
|
629
|
+
let sanitized = version
|
|
630
|
+
.chars()
|
|
631
|
+
.map(|char| {
|
|
632
|
+
if char.is_ascii_alphanumeric() || matches!(char, '.' | '+' | '-' | ':' | '~') {
|
|
633
|
+
char
|
|
634
|
+
} else {
|
|
635
|
+
'~'
|
|
636
|
+
}
|
|
637
|
+
})
|
|
638
|
+
.collect::<String>()
|
|
639
|
+
.trim_matches(['-', '~'])
|
|
640
|
+
.to_string();
|
|
641
|
+
|
|
642
|
+
if sanitized.is_empty() {
|
|
643
|
+
"0.1.0".to_string()
|
|
644
|
+
} else {
|
|
645
|
+
sanitized
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
fn debian_arch(arch: &str) -> String {
|
|
650
|
+
match arch {
|
|
651
|
+
"x64" => "amd64".to_string(),
|
|
652
|
+
"ia32" => "i386".to_string(),
|
|
653
|
+
"armv7l" => "armhf".to_string(),
|
|
654
|
+
arch => arch.to_string(),
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
fn single_line(value: &str) -> String {
|
|
659
|
+
value
|
|
660
|
+
.chars()
|
|
661
|
+
.map(|char| {
|
|
662
|
+
if char == '\n' || char == '\r' {
|
|
663
|
+
' '
|
|
664
|
+
} else {
|
|
665
|
+
char
|
|
666
|
+
}
|
|
667
|
+
})
|
|
668
|
+
.collect()
|
|
669
|
+
}
|
|
670
|
+
|
|
266
671
|
#[cfg(unix)]
|
|
267
672
|
fn unix_mode(metadata: &fs::Metadata, _fallback: u32) -> u32 {
|
|
268
673
|
use std::os::unix::fs::PermissionsExt;
|
|
@@ -324,6 +729,7 @@ impl MakeReport {
|
|
|
324
729
|
#[cfg(test)]
|
|
325
730
|
mod tests {
|
|
326
731
|
use super::*;
|
|
732
|
+
use std::io::Read;
|
|
327
733
|
use zip::ZipArchive;
|
|
328
734
|
|
|
329
735
|
#[test]
|
|
@@ -363,6 +769,40 @@ mod tests {
|
|
|
363
769
|
let _ = fs::remove_dir_all(root);
|
|
364
770
|
}
|
|
365
771
|
|
|
772
|
+
#[test]
|
|
773
|
+
fn builds_make_report_for_deb_target() {
|
|
774
|
+
let root = unique_temp_dir("deb-plan");
|
|
775
|
+
write_package_json(&root);
|
|
776
|
+
write_app_file(&root);
|
|
777
|
+
write_fake_electron_dist(&root);
|
|
778
|
+
|
|
779
|
+
let args = MakeArgs {
|
|
780
|
+
cwd: root.clone(),
|
|
781
|
+
out_dir: PathBuf::from("out"),
|
|
782
|
+
name: None,
|
|
783
|
+
platform: Some("linux".to_string()),
|
|
784
|
+
arch: Some("x64".to_string()),
|
|
785
|
+
target: crate::cli::MakeTarget::Deb,
|
|
786
|
+
skip_package: false,
|
|
787
|
+
force: false,
|
|
788
|
+
dry_run: true,
|
|
789
|
+
json: true,
|
|
790
|
+
};
|
|
791
|
+
let report = build_report(&args).expect("report should build");
|
|
792
|
+
|
|
793
|
+
assert_eq!(report.target, "deb");
|
|
794
|
+
assert!(Path::new(report.artifact.as_str()).ends_with(
|
|
795
|
+
PathBuf::from("out")
|
|
796
|
+
.join("make")
|
|
797
|
+
.join("deb")
|
|
798
|
+
.join("linux")
|
|
799
|
+
.join("x64")
|
|
800
|
+
.join("starter-app_0.1.0_amd64.deb")
|
|
801
|
+
));
|
|
802
|
+
|
|
803
|
+
let _ = fs::remove_dir_all(root);
|
|
804
|
+
}
|
|
805
|
+
|
|
366
806
|
#[test]
|
|
367
807
|
fn makes_zip_artifact_after_packaging() {
|
|
368
808
|
let root = unique_temp_dir("execute");
|
|
@@ -405,6 +845,97 @@ mod tests {
|
|
|
405
845
|
let _ = fs::remove_dir_all(root);
|
|
406
846
|
}
|
|
407
847
|
|
|
848
|
+
#[test]
|
|
849
|
+
fn writes_deb_archive_with_control_and_data_members() {
|
|
850
|
+
let root = unique_temp_dir("deb-archive");
|
|
851
|
+
write_package_json(&root);
|
|
852
|
+
write_app_file(&root);
|
|
853
|
+
write_fake_electron_dist(&root);
|
|
854
|
+
|
|
855
|
+
let args = MakeArgs {
|
|
856
|
+
cwd: root.clone(),
|
|
857
|
+
out_dir: PathBuf::from("out"),
|
|
858
|
+
name: None,
|
|
859
|
+
platform: Some("linux".to_string()),
|
|
860
|
+
arch: Some("x64".to_string()),
|
|
861
|
+
target: crate::cli::MakeTarget::Deb,
|
|
862
|
+
skip_package: false,
|
|
863
|
+
force: false,
|
|
864
|
+
dry_run: true,
|
|
865
|
+
json: true,
|
|
866
|
+
};
|
|
867
|
+
let report = build_report(&args).expect("report should build");
|
|
868
|
+
let bundle_dir = Path::new(report.package.bundle_dir().as_str());
|
|
869
|
+
fs::create_dir_all(bundle_dir.join("resources/app"))
|
|
870
|
+
.expect("fake bundle resources should be created");
|
|
871
|
+
fs::write(bundle_dir.join("starter-app"), "").expect("fake binary should be written");
|
|
872
|
+
fs::write(bundle_dir.join("resources/app/package.json"), "{}")
|
|
873
|
+
.expect("fake app package should be written");
|
|
874
|
+
|
|
875
|
+
write_deb_archive(&report.package, Path::new(report.artifact.as_str()))
|
|
876
|
+
.expect("deb should be written");
|
|
877
|
+
|
|
878
|
+
let members = read_ar_members(Path::new(report.artifact.as_str()));
|
|
879
|
+
assert_eq!(
|
|
880
|
+
members.get("debian-binary").map(Vec::as_slice),
|
|
881
|
+
Some(&b"2.0\n"[..])
|
|
882
|
+
);
|
|
883
|
+
|
|
884
|
+
let control = read_tar_file(
|
|
885
|
+
members
|
|
886
|
+
.get("control.tar.gz")
|
|
887
|
+
.expect("control tar should exist"),
|
|
888
|
+
"control",
|
|
889
|
+
);
|
|
890
|
+
assert!(control.contains("Package: starter-app"));
|
|
891
|
+
assert!(control.contains("Architecture: amd64"));
|
|
892
|
+
|
|
893
|
+
let data = members.get("data.tar.gz").expect("data tar should exist");
|
|
894
|
+
assert!(tar_contains(
|
|
895
|
+
data,
|
|
896
|
+
"opt/starter-app/resources/app/package.json"
|
|
897
|
+
));
|
|
898
|
+
assert!(tar_contains(
|
|
899
|
+
data,
|
|
900
|
+
"usr/share/applications/starter-app.desktop"
|
|
901
|
+
));
|
|
902
|
+
assert!(tar_contains(data, "usr/bin/starter-app"));
|
|
903
|
+
|
|
904
|
+
let _ = fs::remove_dir_all(root);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
#[test]
|
|
908
|
+
fn makes_deb_artifact_after_packaging_on_linux() {
|
|
909
|
+
if !cfg!(target_os = "linux") {
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
let root = unique_temp_dir("deb-execute");
|
|
914
|
+
write_package_json(&root);
|
|
915
|
+
write_app_file(&root);
|
|
916
|
+
write_fake_electron_dist(&root);
|
|
917
|
+
|
|
918
|
+
let args = MakeArgs {
|
|
919
|
+
cwd: root.clone(),
|
|
920
|
+
out_dir: PathBuf::from("out"),
|
|
921
|
+
name: None,
|
|
922
|
+
platform: None,
|
|
923
|
+
arch: None,
|
|
924
|
+
target: crate::cli::MakeTarget::Deb,
|
|
925
|
+
skip_package: false,
|
|
926
|
+
force: false,
|
|
927
|
+
dry_run: false,
|
|
928
|
+
json: false,
|
|
929
|
+
};
|
|
930
|
+
let mut report = build_report(&args).expect("report should build");
|
|
931
|
+
|
|
932
|
+
execute_make(&mut report, &args).expect("make should succeed");
|
|
933
|
+
|
|
934
|
+
assert!(Path::new(report.artifact.as_str()).exists());
|
|
935
|
+
|
|
936
|
+
let _ = fs::remove_dir_all(root);
|
|
937
|
+
}
|
|
938
|
+
|
|
408
939
|
fn write_package_json(root: &Path) {
|
|
409
940
|
fs::write(
|
|
410
941
|
root.join("package.json"),
|
|
@@ -446,4 +977,71 @@ mod tests {
|
|
|
446
977
|
fs::create_dir_all(&path).expect("temp dir should be created");
|
|
447
978
|
path
|
|
448
979
|
}
|
|
980
|
+
|
|
981
|
+
fn read_ar_members(path: &Path) -> std::collections::BTreeMap<String, Vec<u8>> {
|
|
982
|
+
let bytes = fs::read(path).expect("ar archive should be readable");
|
|
983
|
+
assert_eq!(&bytes[..8], b"!<arch>\n");
|
|
984
|
+
|
|
985
|
+
let mut members = std::collections::BTreeMap::new();
|
|
986
|
+
let mut offset = 8;
|
|
987
|
+
while offset < bytes.len() {
|
|
988
|
+
let header = &bytes[offset..offset + 60];
|
|
989
|
+
let name = std::str::from_utf8(&header[0..16])
|
|
990
|
+
.expect("member name should be utf-8")
|
|
991
|
+
.trim()
|
|
992
|
+
.trim_end_matches('/')
|
|
993
|
+
.to_string();
|
|
994
|
+
let size = std::str::from_utf8(&header[48..58])
|
|
995
|
+
.expect("member size should be utf-8")
|
|
996
|
+
.trim()
|
|
997
|
+
.parse::<usize>()
|
|
998
|
+
.expect("member size should parse");
|
|
999
|
+
let data_start = offset + 60;
|
|
1000
|
+
let data_end = data_start + size;
|
|
1001
|
+
members.insert(name, bytes[data_start..data_end].to_vec());
|
|
1002
|
+
offset = data_end + (size % 2);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
members
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
fn read_tar_file(archive: &[u8], path: &str) -> String {
|
|
1009
|
+
let decoder = flate2::read::GzDecoder::new(archive);
|
|
1010
|
+
let mut archive = tar::Archive::new(decoder);
|
|
1011
|
+
for entry in archive.entries().expect("tar entries should read") {
|
|
1012
|
+
let mut entry = entry.expect("tar entry should read");
|
|
1013
|
+
let entry_path = entry
|
|
1014
|
+
.path()
|
|
1015
|
+
.expect("tar path should read")
|
|
1016
|
+
.to_string_lossy()
|
|
1017
|
+
.trim_start_matches("./")
|
|
1018
|
+
.to_string();
|
|
1019
|
+
if entry_path == path {
|
|
1020
|
+
let mut contents = String::new();
|
|
1021
|
+
entry
|
|
1022
|
+
.read_to_string(&mut contents)
|
|
1023
|
+
.expect("tar file should read");
|
|
1024
|
+
return contents;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
panic!("tar file was not found: {path}");
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
fn tar_contains(archive: &[u8], path: &str) -> bool {
|
|
1032
|
+
let decoder = flate2::read::GzDecoder::new(archive);
|
|
1033
|
+
let mut archive = tar::Archive::new(decoder);
|
|
1034
|
+
archive
|
|
1035
|
+
.entries()
|
|
1036
|
+
.expect("tar entries should read")
|
|
1037
|
+
.any(|entry| {
|
|
1038
|
+
entry
|
|
1039
|
+
.expect("tar entry should read")
|
|
1040
|
+
.path()
|
|
1041
|
+
.expect("tar path should read")
|
|
1042
|
+
.to_string_lossy()
|
|
1043
|
+
.trim_start_matches("./")
|
|
1044
|
+
== path
|
|
1045
|
+
})
|
|
1046
|
+
}
|
|
449
1047
|
}
|
package/src/commands/package.rs
CHANGED
|
@@ -1177,6 +1177,10 @@ impl PackageReport {
|
|
|
1177
1177
|
&self.app_name
|
|
1178
1178
|
}
|
|
1179
1179
|
|
|
1180
|
+
pub(crate) fn executable_name(&self) -> &str {
|
|
1181
|
+
&self.executable_name
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1180
1184
|
pub(crate) fn artifact_stem(&self) -> String {
|
|
1181
1185
|
sanitize_artifact_name(&self.app_name)
|
|
1182
1186
|
}
|