electron-cli 0.3.0-alpha.0 → 0.3.0-alpha.10
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 +1227 -67
- package/Cargo.toml +11 -2
- package/README.md +64 -10
- package/bin/electron-cli.js +6 -3
- package/package.json +8 -5
- package/scripts/install.js +101 -0
- package/src/cli.rs +260 -1
- package/src/commands/init.rs +814 -0
- package/src/commands/make.rs +1710 -0
- package/src/commands/mod.rs +5 -0
- package/src/commands/package.rs +1626 -0
- package/src/commands/plan.rs +64 -5
- package/src/commands/publish.rs +432 -0
- package/src/commands/start.rs +287 -0
- package/src/main.rs +5 -0
- package/src/project.rs +6 -0
- 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,1710 @@
|
|
|
1
|
+
use std::{
|
|
2
|
+
fs,
|
|
3
|
+
fs::File,
|
|
4
|
+
io::{self, BufWriter, Cursor, Write},
|
|
5
|
+
path::{Path, PathBuf},
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
use anyhow::{bail, Context, Result};
|
|
9
|
+
use camino::Utf8PathBuf;
|
|
10
|
+
use fatfs::{Dir as FatDir, FileSystem, FormatVolumeOptions, FsOptions, ReadWriteSeek};
|
|
11
|
+
use flate2::{write::GzEncoder, Compression};
|
|
12
|
+
use fscommon::BufStream;
|
|
13
|
+
use rpm::{BuildConfig, CompressionType, FileOptions, PackageBuilder};
|
|
14
|
+
use serde::Serialize;
|
|
15
|
+
use tar::{Builder as TarBuilder, Header as TarHeader};
|
|
16
|
+
use zip::{write::SimpleFileOptions, CompressionMethod, ZipWriter};
|
|
17
|
+
|
|
18
|
+
use crate::{
|
|
19
|
+
cli::{MakeArgs, MakeTarget, PackageArgs},
|
|
20
|
+
commands::package::{self, PackageReport},
|
|
21
|
+
output,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
#[derive(Debug, Serialize)]
|
|
25
|
+
pub(crate) struct MakeReport {
|
|
26
|
+
package: PackageReport,
|
|
27
|
+
target: String,
|
|
28
|
+
skip_package: bool,
|
|
29
|
+
dry_run: bool,
|
|
30
|
+
make_dir: Utf8PathBuf,
|
|
31
|
+
artifact: Utf8PathBuf,
|
|
32
|
+
artifact_size: Option<u64>,
|
|
33
|
+
status: MakeStatus,
|
|
34
|
+
warnings: Vec<String>,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#[derive(Debug, Serialize)]
|
|
38
|
+
#[serde(rename_all = "kebab-case")]
|
|
39
|
+
enum MakeStatus {
|
|
40
|
+
Planned,
|
|
41
|
+
Made,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
pub fn run(args: MakeArgs) -> Result<()> {
|
|
45
|
+
let mut report = build_report(&args)?;
|
|
46
|
+
|
|
47
|
+
if args.dry_run {
|
|
48
|
+
return print_report(&report, args.json);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
execute_make(&mut report, &args)?;
|
|
52
|
+
report.mark_made()?;
|
|
53
|
+
|
|
54
|
+
print_report(&report, args.json)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
pub(crate) fn build_report(args: &MakeArgs) -> Result<MakeReport> {
|
|
58
|
+
let package_args = PackageArgs {
|
|
59
|
+
cwd: args.cwd.clone(),
|
|
60
|
+
out_dir: args.out_dir.clone(),
|
|
61
|
+
name: args.name.clone(),
|
|
62
|
+
platform: args.platform.clone(),
|
|
63
|
+
arch: args.arch.clone(),
|
|
64
|
+
force: args.force,
|
|
65
|
+
dry_run: false,
|
|
66
|
+
json: false,
|
|
67
|
+
};
|
|
68
|
+
let snapshot = crate::project::inspect(&package_args.cwd)?;
|
|
69
|
+
let package = package::build_report(snapshot, &package_args)?;
|
|
70
|
+
let make_dir = Path::new(package.output_dir().as_str())
|
|
71
|
+
.join("make")
|
|
72
|
+
.join(args.target.as_str())
|
|
73
|
+
.join(package.platform())
|
|
74
|
+
.join(package.arch());
|
|
75
|
+
let artifact = make_artifact_path(&make_dir, &package, args.target);
|
|
76
|
+
|
|
77
|
+
let mut warnings = package.warnings().to_vec();
|
|
78
|
+
if matches!(args.target, MakeTarget::Deb | MakeTarget::Rpm) && package.platform() != "linux" {
|
|
79
|
+
warnings.push(format!(
|
|
80
|
+
"{} maker only supports linux packages; target platform is {}.",
|
|
81
|
+
args.target.as_str(),
|
|
82
|
+
package.platform()
|
|
83
|
+
));
|
|
84
|
+
}
|
|
85
|
+
if args.target == MakeTarget::Dmg && package.platform() != "darwin" {
|
|
86
|
+
warnings.push(format!(
|
|
87
|
+
"dmg maker only supports macOS packages; target platform is {}.",
|
|
88
|
+
package.platform()
|
|
89
|
+
));
|
|
90
|
+
}
|
|
91
|
+
if args.skip_package && !Path::new(package.bundle_dir().as_str()).exists() {
|
|
92
|
+
warnings.push(format!(
|
|
93
|
+
"Package output does not exist: {}.",
|
|
94
|
+
package.bundle_dir()
|
|
95
|
+
));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if artifact.exists() && !args.force {
|
|
99
|
+
warnings.push(format!(
|
|
100
|
+
"Make artifact already exists: {}. Use --force to overwrite it.",
|
|
101
|
+
artifact.display()
|
|
102
|
+
));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
Ok(MakeReport {
|
|
106
|
+
package,
|
|
107
|
+
target: args.target.as_str().to_string(),
|
|
108
|
+
skip_package: args.skip_package,
|
|
109
|
+
dry_run: args.dry_run,
|
|
110
|
+
make_dir: utf8_path(make_dir)?,
|
|
111
|
+
artifact: utf8_path(artifact)?,
|
|
112
|
+
artifact_size: None,
|
|
113
|
+
status: MakeStatus::Planned,
|
|
114
|
+
warnings,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
pub(crate) fn execute_make(report: &mut MakeReport, args: &MakeArgs) -> Result<()> {
|
|
119
|
+
if !args.skip_package {
|
|
120
|
+
package::execute_package(&report.package, args.force)?;
|
|
121
|
+
report.package.mark_packaged();
|
|
122
|
+
} else if !Path::new(report.package.bundle_dir().as_str()).exists() {
|
|
123
|
+
bail!(
|
|
124
|
+
"Package output does not exist: {}. Run without --skip-package or run electron-cli package first.",
|
|
125
|
+
report.package.bundle_dir()
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let artifact = Path::new(report.artifact.as_str());
|
|
130
|
+
if artifact.exists() {
|
|
131
|
+
if args.force {
|
|
132
|
+
fs::remove_file(artifact)
|
|
133
|
+
.with_context(|| format!("Could not remove {}", artifact.display()))?;
|
|
134
|
+
} else {
|
|
135
|
+
bail!(
|
|
136
|
+
"Make artifact already exists: {}. Use --force to overwrite it.",
|
|
137
|
+
artifact.display()
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fs::create_dir_all(report.make_dir.as_str())
|
|
143
|
+
.with_context(|| format!("Could not create {}", report.make_dir))?;
|
|
144
|
+
match args.target {
|
|
145
|
+
MakeTarget::Zip => {
|
|
146
|
+
write_zip_archive(Path::new(report.package.bundle_dir().as_str()), artifact)?
|
|
147
|
+
}
|
|
148
|
+
MakeTarget::Deb => write_deb_archive(&report.package, artifact)?,
|
|
149
|
+
MakeTarget::Dmg => write_dmg_archive(&report.package, artifact)?,
|
|
150
|
+
MakeTarget::Rpm => write_rpm_archive(&report.package, artifact)?,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
Ok(())
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fn print_report(report: &MakeReport, json: bool) -> Result<()> {
|
|
157
|
+
if json {
|
|
158
|
+
return output::json(report);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
println!("electron-cli make");
|
|
162
|
+
println!();
|
|
163
|
+
println!("Project");
|
|
164
|
+
println!(" root: {}", report.package.project().root);
|
|
165
|
+
match report.package.project().package_label() {
|
|
166
|
+
Some(label) => println!(" package: {label}"),
|
|
167
|
+
None => println!(" package: not found"),
|
|
168
|
+
}
|
|
169
|
+
println!(" app name: {}", report.package.app_name());
|
|
170
|
+
println!(
|
|
171
|
+
" target: {} {} {}",
|
|
172
|
+
report.target,
|
|
173
|
+
report.package.platform(),
|
|
174
|
+
report.package.arch()
|
|
175
|
+
);
|
|
176
|
+
println!(" status: {}", report.status.as_str());
|
|
177
|
+
|
|
178
|
+
println!();
|
|
179
|
+
println!("Artifact");
|
|
180
|
+
println!(" {}", report.artifact);
|
|
181
|
+
if let Some(size) = report.artifact_size {
|
|
182
|
+
println!(" size: {size} bytes");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if !report.warnings.is_empty() {
|
|
186
|
+
println!();
|
|
187
|
+
println!("Warnings");
|
|
188
|
+
for warning in &report.warnings {
|
|
189
|
+
println!(" {warning}");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
Ok(())
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fn make_artifact_path(make_dir: &Path, package: &PackageReport, target: MakeTarget) -> PathBuf {
|
|
197
|
+
match target {
|
|
198
|
+
MakeTarget::Zip => make_dir.join(format!(
|
|
199
|
+
"{}-{}-{}.zip",
|
|
200
|
+
package.artifact_stem(),
|
|
201
|
+
package.platform(),
|
|
202
|
+
package.arch()
|
|
203
|
+
)),
|
|
204
|
+
MakeTarget::Deb => make_dir.join(format!(
|
|
205
|
+
"{}_{}_{}.deb",
|
|
206
|
+
debian_package_name(&package.artifact_stem()),
|
|
207
|
+
debian_version(package.project().version.as_deref()),
|
|
208
|
+
debian_arch(package.arch())
|
|
209
|
+
)),
|
|
210
|
+
MakeTarget::Dmg => make_dir.join(format!(
|
|
211
|
+
"{}-{}-{}.dmg",
|
|
212
|
+
package.artifact_stem(),
|
|
213
|
+
dmg_version(package.project().version.as_deref()),
|
|
214
|
+
package.arch()
|
|
215
|
+
)),
|
|
216
|
+
MakeTarget::Rpm => make_dir.join(format!(
|
|
217
|
+
"{}-{}-1.{}.rpm",
|
|
218
|
+
rpm_package_name(&package.artifact_stem()),
|
|
219
|
+
rpm_version(package.project().version.as_deref()),
|
|
220
|
+
rpm_arch(package.arch())
|
|
221
|
+
)),
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
fn write_zip_archive(source: &Path, artifact: &Path) -> Result<()> {
|
|
226
|
+
if !source.exists() {
|
|
227
|
+
bail!("Package output does not exist: {}", source.display());
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let parent = artifact
|
|
231
|
+
.parent()
|
|
232
|
+
.with_context(|| format!("Artifact path has no parent: {}", artifact.display()))?;
|
|
233
|
+
fs::create_dir_all(parent).with_context(|| format!("Could not create {}", parent.display()))?;
|
|
234
|
+
|
|
235
|
+
let file = File::create(artifact)
|
|
236
|
+
.with_context(|| format!("Could not create {}", artifact.display()))?;
|
|
237
|
+
let mut writer = ZipWriter::new(BufWriter::new(file));
|
|
238
|
+
let base = source
|
|
239
|
+
.parent()
|
|
240
|
+
.with_context(|| format!("Package output has no parent: {}", source.display()))?;
|
|
241
|
+
|
|
242
|
+
add_path_to_zip(source, base, &mut writer)?;
|
|
243
|
+
writer
|
|
244
|
+
.finish()
|
|
245
|
+
.with_context(|| format!("Could not finish {}", artifact.display()))?;
|
|
246
|
+
|
|
247
|
+
Ok(())
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
fn write_dmg_archive(package: &PackageReport, artifact: &Path) -> Result<()> {
|
|
251
|
+
if package.platform() != "darwin" {
|
|
252
|
+
bail!(
|
|
253
|
+
"DMG maker only supports macOS packages. Requested {}.",
|
|
254
|
+
package.platform()
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let source = Path::new(package.bundle_dir().as_str());
|
|
259
|
+
if !source.exists() {
|
|
260
|
+
bail!("Package output does not exist: {}", source.display());
|
|
261
|
+
}
|
|
262
|
+
if source.extension().and_then(|extension| extension.to_str()) != Some("app") {
|
|
263
|
+
bail!(
|
|
264
|
+
"DMG maker expected a macOS .app bundle: {}",
|
|
265
|
+
source.display()
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let parent = artifact
|
|
270
|
+
.parent()
|
|
271
|
+
.with_context(|| format!("Artifact path has no parent: {}", artifact.display()))?;
|
|
272
|
+
fs::create_dir_all(parent).with_context(|| format!("Could not create {}", parent.display()))?;
|
|
273
|
+
|
|
274
|
+
let volume_label = fat_volume_label(package.app_name());
|
|
275
|
+
let fat32 = create_dmg_fat32(source, &volume_label)?;
|
|
276
|
+
apple_dmg::DmgWriter::create(artifact)
|
|
277
|
+
.with_context(|| format!("Could not create {}", artifact.display()))?
|
|
278
|
+
.create_fat32(&fat32)
|
|
279
|
+
.with_context(|| format!("Could not write {}", artifact.display()))
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const DMG_SECTOR_SIZE: u64 = 512;
|
|
283
|
+
const DMG_MIN_BYTES: u64 = 64 * 1024 * 1024;
|
|
284
|
+
const DMG_SECTOR_ALIGNMENT: u64 = 2048;
|
|
285
|
+
|
|
286
|
+
fn create_dmg_fat32(app_bundle: &Path, volume_label: &[u8; 11]) -> Result<Vec<u8>> {
|
|
287
|
+
let total_sectors = dmg_total_sectors(app_bundle)?;
|
|
288
|
+
let mut fat32 = vec![0; total_sectors as usize * DMG_SECTOR_SIZE as usize];
|
|
289
|
+
|
|
290
|
+
{
|
|
291
|
+
let volume_options = FormatVolumeOptions::new()
|
|
292
|
+
.volume_label(*volume_label)
|
|
293
|
+
.bytes_per_sector(DMG_SECTOR_SIZE as u16)
|
|
294
|
+
.total_sectors(total_sectors);
|
|
295
|
+
let mut disk = BufStream::new(Cursor::new(&mut fat32));
|
|
296
|
+
fatfs::format_volume(&mut disk, volume_options)
|
|
297
|
+
.context("Could not format DMG FAT32 volume")?;
|
|
298
|
+
drop(disk);
|
|
299
|
+
|
|
300
|
+
let disk = BufStream::new(Cursor::new(&mut fat32));
|
|
301
|
+
let fs =
|
|
302
|
+
FileSystem::new(disk, FsOptions::new()).context("Could not open DMG FAT32 volume")?;
|
|
303
|
+
let root = fs.root_dir();
|
|
304
|
+
let app_name = utf8_file_name(app_bundle)?;
|
|
305
|
+
let app_dir = root
|
|
306
|
+
.create_dir(app_name)
|
|
307
|
+
.with_context(|| format!("Could not add {app_name} to DMG"))?;
|
|
308
|
+
add_directory_to_fat(app_bundle, &app_dir)
|
|
309
|
+
.with_context(|| format!("Could not add {} to DMG", app_bundle.display()))?;
|
|
310
|
+
write_fat_symlink(&root, "Applications", "/Applications")
|
|
311
|
+
.context("Could not add Applications link to DMG")?;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
Ok(fat32)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
fn dmg_total_sectors(app_bundle: &Path) -> Result<u32> {
|
|
318
|
+
let stats = directory_stats(app_bundle)?;
|
|
319
|
+
let cluster_slack_estimate =
|
|
320
|
+
stats.files.saturating_mul(16 * 1024) + stats.directories.saturating_mul(4096);
|
|
321
|
+
let payload_estimate = stats
|
|
322
|
+
.bytes
|
|
323
|
+
.saturating_add(cluster_slack_estimate)
|
|
324
|
+
.saturating_add(16 * 1024 * 1024);
|
|
325
|
+
let required_bytes = payload_estimate
|
|
326
|
+
.saturating_add(payload_estimate / 3)
|
|
327
|
+
.max(DMG_MIN_BYTES);
|
|
328
|
+
let sectors = required_bytes.div_ceil(DMG_SECTOR_SIZE);
|
|
329
|
+
let aligned_sectors = sectors.div_ceil(DMG_SECTOR_ALIGNMENT) * DMG_SECTOR_ALIGNMENT;
|
|
330
|
+
u32::try_from(aligned_sectors).context("DMG contents are too large for a FAT32 image")
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
#[derive(Default)]
|
|
334
|
+
struct DirectoryStats {
|
|
335
|
+
bytes: u64,
|
|
336
|
+
files: u64,
|
|
337
|
+
directories: u64,
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
fn directory_stats(path: &Path) -> Result<DirectoryStats> {
|
|
341
|
+
let metadata =
|
|
342
|
+
fs::symlink_metadata(path).with_context(|| format!("Could not stat {}", path.display()))?;
|
|
343
|
+
if metadata.is_file() {
|
|
344
|
+
return Ok(DirectoryStats {
|
|
345
|
+
bytes: metadata.len(),
|
|
346
|
+
files: 1,
|
|
347
|
+
directories: 0,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
if metadata.file_type().is_symlink() {
|
|
351
|
+
return Ok(DirectoryStats {
|
|
352
|
+
bytes: read_link_lossy(path)?.len() as u64,
|
|
353
|
+
files: 1,
|
|
354
|
+
directories: 0,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
if !metadata.is_dir() {
|
|
358
|
+
return Ok(DirectoryStats::default());
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
let mut stats = DirectoryStats {
|
|
362
|
+
bytes: 0,
|
|
363
|
+
files: 0,
|
|
364
|
+
directories: 1,
|
|
365
|
+
};
|
|
366
|
+
for entry in fs::read_dir(path).with_context(|| format!("Could not read {}", path.display()))? {
|
|
367
|
+
let entry = entry?;
|
|
368
|
+
let child = directory_stats(&entry.path())?;
|
|
369
|
+
stats.bytes = stats.bytes.saturating_add(child.bytes);
|
|
370
|
+
stats.files = stats.files.saturating_add(child.files);
|
|
371
|
+
stats.directories = stats.directories.saturating_add(child.directories);
|
|
372
|
+
}
|
|
373
|
+
Ok(stats)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
fn add_directory_to_fat<T: ReadWriteSeek>(
|
|
377
|
+
source: &Path,
|
|
378
|
+
destination: &FatDir<'_, T>,
|
|
379
|
+
) -> Result<()> {
|
|
380
|
+
let mut entries = fs::read_dir(source)
|
|
381
|
+
.with_context(|| format!("Could not read {}", source.display()))?
|
|
382
|
+
.collect::<Result<Vec<_>, io::Error>>()?;
|
|
383
|
+
entries.sort_by_key(|entry| entry.path());
|
|
384
|
+
|
|
385
|
+
for entry in entries {
|
|
386
|
+
let source_path = entry.path();
|
|
387
|
+
let file_name = utf8_file_name(&source_path)?;
|
|
388
|
+
let metadata = fs::symlink_metadata(&source_path)
|
|
389
|
+
.with_context(|| format!("Could not stat {}", source_path.display()))?;
|
|
390
|
+
|
|
391
|
+
if metadata.is_dir() {
|
|
392
|
+
let child = destination
|
|
393
|
+
.create_dir(file_name)
|
|
394
|
+
.with_context(|| format!("Could not create DMG directory {file_name}"))?;
|
|
395
|
+
add_directory_to_fat(&source_path, &child)?;
|
|
396
|
+
} else if metadata.file_type().is_symlink() {
|
|
397
|
+
let target = read_link_lossy(&source_path)?;
|
|
398
|
+
write_fat_symlink(destination, file_name, &target)?;
|
|
399
|
+
} else if metadata.is_file() {
|
|
400
|
+
let mut source_file = File::open(&source_path)
|
|
401
|
+
.with_context(|| format!("Could not open {}", source_path.display()))?;
|
|
402
|
+
let mut destination_file = destination
|
|
403
|
+
.create_file(file_name)
|
|
404
|
+
.with_context(|| format!("Could not create DMG file {file_name}"))?;
|
|
405
|
+
io::copy(&mut source_file, &mut destination_file)
|
|
406
|
+
.with_context(|| format!("Could not write DMG file {file_name}"))?;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
Ok(())
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
fn write_fat_symlink<T: ReadWriteSeek>(
|
|
414
|
+
directory: &FatDir<'_, T>,
|
|
415
|
+
name: &str,
|
|
416
|
+
target: &str,
|
|
417
|
+
) -> Result<()> {
|
|
418
|
+
let bytes = fat_symlink_bytes(target)?;
|
|
419
|
+
let mut file = directory
|
|
420
|
+
.create_file(name)
|
|
421
|
+
.with_context(|| format!("Could not create DMG symlink {name}"))?;
|
|
422
|
+
file.write_all(&bytes)
|
|
423
|
+
.with_context(|| format!("Could not write DMG symlink {name}"))
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
fn fat_symlink_bytes(target: &str) -> Result<Vec<u8>> {
|
|
427
|
+
let mut bytes = format!(
|
|
428
|
+
"XSym\n{:04}\n{:x}\n{}\n",
|
|
429
|
+
target.len(),
|
|
430
|
+
md5::compute(target.as_bytes()),
|
|
431
|
+
target
|
|
432
|
+
)
|
|
433
|
+
.into_bytes();
|
|
434
|
+
anyhow::ensure!(bytes.len() <= 1067, "Symlink target is too long: {target}");
|
|
435
|
+
bytes.resize(1067, b' ');
|
|
436
|
+
Ok(bytes)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
fn fat_volume_label(name: &str) -> [u8; 11] {
|
|
440
|
+
let mut label = [b' '; 11];
|
|
441
|
+
let sanitized = name
|
|
442
|
+
.to_ascii_uppercase()
|
|
443
|
+
.bytes()
|
|
444
|
+
.filter(|byte| byte.is_ascii_alphanumeric())
|
|
445
|
+
.take(11)
|
|
446
|
+
.collect::<Vec<_>>();
|
|
447
|
+
if sanitized.is_empty() {
|
|
448
|
+
label[..3].copy_from_slice(b"APP");
|
|
449
|
+
} else {
|
|
450
|
+
label[..sanitized.len()].copy_from_slice(&sanitized);
|
|
451
|
+
}
|
|
452
|
+
label
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
fn read_link_lossy(path: &Path) -> Result<String> {
|
|
456
|
+
Ok(fs::read_link(path)
|
|
457
|
+
.with_context(|| format!("Could not read link {}", path.display()))?
|
|
458
|
+
.to_string_lossy()
|
|
459
|
+
.to_string())
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
fn utf8_file_name(path: &Path) -> Result<&str> {
|
|
463
|
+
path.file_name()
|
|
464
|
+
.and_then(|file_name| file_name.to_str())
|
|
465
|
+
.with_context(|| format!("Path has no UTF-8 file name: {}", path.display()))
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
fn add_path_to_zip(
|
|
469
|
+
path: &Path,
|
|
470
|
+
base: &Path,
|
|
471
|
+
writer: &mut ZipWriter<BufWriter<File>>,
|
|
472
|
+
) -> Result<()> {
|
|
473
|
+
let metadata =
|
|
474
|
+
fs::metadata(path).with_context(|| format!("Could not stat {}", path.display()))?;
|
|
475
|
+
let relative_path = zip_relative_path(path, base)?;
|
|
476
|
+
|
|
477
|
+
if metadata.is_dir() {
|
|
478
|
+
if !relative_path.is_empty() {
|
|
479
|
+
let directory_name = format!("{relative_path}/");
|
|
480
|
+
writer
|
|
481
|
+
.add_directory(directory_name, directory_options(&metadata))
|
|
482
|
+
.with_context(|| format!("Could not add {} to archive", path.display()))?;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
let mut entries = fs::read_dir(path)
|
|
486
|
+
.with_context(|| format!("Could not read {}", path.display()))?
|
|
487
|
+
.collect::<Result<Vec<_>, io::Error>>()?;
|
|
488
|
+
entries.sort_by_key(|entry| entry.path());
|
|
489
|
+
|
|
490
|
+
for entry in entries {
|
|
491
|
+
add_path_to_zip(&entry.path(), base, writer)?;
|
|
492
|
+
}
|
|
493
|
+
} else {
|
|
494
|
+
writer
|
|
495
|
+
.start_file(relative_path, file_options(&metadata))
|
|
496
|
+
.with_context(|| format!("Could not add {} to archive", path.display()))?;
|
|
497
|
+
let mut file =
|
|
498
|
+
File::open(path).with_context(|| format!("Could not open {}", path.display()))?;
|
|
499
|
+
io::copy(&mut file, writer)
|
|
500
|
+
.with_context(|| format!("Could not write {} to archive", path.display()))?;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
Ok(())
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
fn zip_relative_path(path: &Path, base: &Path) -> Result<String> {
|
|
507
|
+
let relative = path.strip_prefix(base).with_context(|| {
|
|
508
|
+
format!(
|
|
509
|
+
"Could not make {} relative to {}",
|
|
510
|
+
path.display(),
|
|
511
|
+
base.display()
|
|
512
|
+
)
|
|
513
|
+
})?;
|
|
514
|
+
Ok(relative
|
|
515
|
+
.components()
|
|
516
|
+
.map(|component| component.as_os_str().to_string_lossy())
|
|
517
|
+
.collect::<Vec<_>>()
|
|
518
|
+
.join("/"))
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
fn file_options(metadata: &fs::Metadata) -> SimpleFileOptions {
|
|
522
|
+
SimpleFileOptions::default()
|
|
523
|
+
.compression_method(CompressionMethod::Deflated)
|
|
524
|
+
.unix_permissions(unix_mode(metadata, 0o644))
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
fn directory_options(metadata: &fs::Metadata) -> SimpleFileOptions {
|
|
528
|
+
SimpleFileOptions::default()
|
|
529
|
+
.compression_method(CompressionMethod::Stored)
|
|
530
|
+
.unix_permissions(unix_mode(metadata, 0o755))
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
fn write_deb_archive(package: &PackageReport, artifact: &Path) -> Result<()> {
|
|
534
|
+
if package.platform() != "linux" {
|
|
535
|
+
bail!(
|
|
536
|
+
"Deb maker only supports linux packages. Requested {}.",
|
|
537
|
+
package.platform()
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
let source = Path::new(package.bundle_dir().as_str());
|
|
542
|
+
if !source.exists() {
|
|
543
|
+
bail!("Package output does not exist: {}", source.display());
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
let parent = artifact
|
|
547
|
+
.parent()
|
|
548
|
+
.with_context(|| format!("Artifact path has no parent: {}", artifact.display()))?;
|
|
549
|
+
fs::create_dir_all(parent).with_context(|| format!("Could not create {}", parent.display()))?;
|
|
550
|
+
|
|
551
|
+
let deb_package = debian_package_name(&package.artifact_stem());
|
|
552
|
+
let version = debian_version(package.project().version.as_deref());
|
|
553
|
+
let arch = debian_arch(package.arch());
|
|
554
|
+
let installed_size = directory_size(source)?.div_ceil(1024).max(1);
|
|
555
|
+
let control = debian_control_file(package, &deb_package, &version, &arch, installed_size);
|
|
556
|
+
let control_tar =
|
|
557
|
+
gzip_tar(|builder| append_bytes_to_tar(builder, "./control", control.as_bytes(), 0o644))?;
|
|
558
|
+
let data_tar = gzip_tar(|builder| append_deb_data_tar(builder, package, source, &deb_package))?;
|
|
559
|
+
|
|
560
|
+
write_ar_archive(
|
|
561
|
+
artifact,
|
|
562
|
+
&[
|
|
563
|
+
ArMember {
|
|
564
|
+
name: "debian-binary",
|
|
565
|
+
mode: 0o100644,
|
|
566
|
+
data: b"2.0\n".to_vec(),
|
|
567
|
+
},
|
|
568
|
+
ArMember {
|
|
569
|
+
name: "control.tar.gz",
|
|
570
|
+
mode: 0o100644,
|
|
571
|
+
data: control_tar,
|
|
572
|
+
},
|
|
573
|
+
ArMember {
|
|
574
|
+
name: "data.tar.gz",
|
|
575
|
+
mode: 0o100644,
|
|
576
|
+
data: data_tar,
|
|
577
|
+
},
|
|
578
|
+
],
|
|
579
|
+
)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
fn write_rpm_archive(package: &PackageReport, artifact: &Path) -> Result<()> {
|
|
583
|
+
if package.platform() != "linux" {
|
|
584
|
+
bail!(
|
|
585
|
+
"RPM maker only supports linux packages. Requested {}.",
|
|
586
|
+
package.platform()
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
let source = Path::new(package.bundle_dir().as_str());
|
|
591
|
+
if !source.exists() {
|
|
592
|
+
bail!("Package output does not exist: {}", source.display());
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
let parent = artifact
|
|
596
|
+
.parent()
|
|
597
|
+
.with_context(|| format!("Artifact path has no parent: {}", artifact.display()))?;
|
|
598
|
+
fs::create_dir_all(parent).with_context(|| format!("Could not create {}", parent.display()))?;
|
|
599
|
+
|
|
600
|
+
let rpm_package = rpm_package_name(&package.artifact_stem());
|
|
601
|
+
let version = rpm_version(package.project().version.as_deref());
|
|
602
|
+
let arch = rpm_arch(package.arch());
|
|
603
|
+
let executable = format!("/opt/{rpm_package}/{}", package.executable_name());
|
|
604
|
+
let mut builder = PackageBuilder::new(
|
|
605
|
+
&rpm_package,
|
|
606
|
+
&version,
|
|
607
|
+
package
|
|
608
|
+
.project()
|
|
609
|
+
.license
|
|
610
|
+
.as_deref()
|
|
611
|
+
.unwrap_or("LicenseRef-unknown"),
|
|
612
|
+
&arch,
|
|
613
|
+
&single_line(package.app_name()),
|
|
614
|
+
);
|
|
615
|
+
builder
|
|
616
|
+
.using_config(
|
|
617
|
+
BuildConfig::v4()
|
|
618
|
+
.compression(CompressionType::Gzip)
|
|
619
|
+
.reserved_space(None)
|
|
620
|
+
.source_date(0),
|
|
621
|
+
)
|
|
622
|
+
.release("1")
|
|
623
|
+
.vendor("electron-cli")
|
|
624
|
+
.packager("electron-cli")
|
|
625
|
+
.description(format!(
|
|
626
|
+
"{} packaged by electron-cli.",
|
|
627
|
+
single_line(package.app_name())
|
|
628
|
+
))
|
|
629
|
+
.default_file_attrs(None, Some("root".to_string()), Some("root".to_string()))
|
|
630
|
+
.default_dir_attrs(None, Some("root".to_string()), Some("root".to_string()));
|
|
631
|
+
|
|
632
|
+
for directory in [
|
|
633
|
+
"/opt",
|
|
634
|
+
"/usr",
|
|
635
|
+
"/usr/bin",
|
|
636
|
+
"/usr/share",
|
|
637
|
+
"/usr/share/applications",
|
|
638
|
+
] {
|
|
639
|
+
builder.with_dir_entry(FileOptions::dir(directory).permissions(0o755))?;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
builder.with_dir(source, format!("/opt/{rpm_package}"), |options| options)?;
|
|
643
|
+
builder.with_symlink(FileOptions::symlink(
|
|
644
|
+
format!("/usr/bin/{rpm_package}"),
|
|
645
|
+
&executable,
|
|
646
|
+
))?;
|
|
647
|
+
builder.with_file_contents(
|
|
648
|
+
rpm_desktop_file(package, &rpm_package, &executable),
|
|
649
|
+
FileOptions::new(format!("/usr/share/applications/{rpm_package}.desktop"))
|
|
650
|
+
.permissions(0o644),
|
|
651
|
+
)?;
|
|
652
|
+
|
|
653
|
+
let rpm = builder.build()?;
|
|
654
|
+
rpm.write_file(artifact)
|
|
655
|
+
.with_context(|| format!("Could not write {}", artifact.display()))
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
fn debian_control_file(
|
|
659
|
+
package: &PackageReport,
|
|
660
|
+
deb_package: &str,
|
|
661
|
+
version: &str,
|
|
662
|
+
arch: &str,
|
|
663
|
+
installed_size: u64,
|
|
664
|
+
) -> String {
|
|
665
|
+
format!(
|
|
666
|
+
"Package: {deb_package}\n\
|
|
667
|
+
Version: {version}\n\
|
|
668
|
+
Section: utils\n\
|
|
669
|
+
Priority: optional\n\
|
|
670
|
+
Architecture: {arch}\n\
|
|
671
|
+
Maintainer: electron-cli <noreply@example.invalid>\n\
|
|
672
|
+
Installed-Size: {installed_size}\n\
|
|
673
|
+
Description: {description}\n\
|
|
674
|
+
Electron application packaged by electron-cli.\n",
|
|
675
|
+
description = single_line(package.app_name())
|
|
676
|
+
)
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
fn append_deb_data_tar(
|
|
680
|
+
builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
|
|
681
|
+
package: &PackageReport,
|
|
682
|
+
source: &Path,
|
|
683
|
+
deb_package: &str,
|
|
684
|
+
) -> Result<()> {
|
|
685
|
+
for directory in [
|
|
686
|
+
"./",
|
|
687
|
+
"./opt",
|
|
688
|
+
"./usr",
|
|
689
|
+
"./usr/bin",
|
|
690
|
+
"./usr/share",
|
|
691
|
+
"./usr/share/applications",
|
|
692
|
+
] {
|
|
693
|
+
append_directory_to_tar(builder, directory, 0o755)?;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
let app_root = format!("./opt/{deb_package}");
|
|
697
|
+
append_directory_to_tar(builder, &app_root, 0o755)?;
|
|
698
|
+
append_directory_contents_to_tar(builder, source, Path::new(&app_root))?;
|
|
699
|
+
|
|
700
|
+
let executable = format!("/opt/{deb_package}/{}", package.executable_name());
|
|
701
|
+
append_symlink_to_tar(
|
|
702
|
+
builder,
|
|
703
|
+
format!("./usr/bin/{deb_package}"),
|
|
704
|
+
&executable,
|
|
705
|
+
0o777,
|
|
706
|
+
)?;
|
|
707
|
+
append_bytes_to_tar(
|
|
708
|
+
builder,
|
|
709
|
+
format!("./usr/share/applications/{deb_package}.desktop"),
|
|
710
|
+
debian_desktop_file(package, deb_package, &executable).as_bytes(),
|
|
711
|
+
0o644,
|
|
712
|
+
)?;
|
|
713
|
+
|
|
714
|
+
Ok(())
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
fn debian_desktop_file(package: &PackageReport, deb_package: &str, executable: &str) -> String {
|
|
718
|
+
desktop_file(package, deb_package, executable)
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
fn rpm_desktop_file(package: &PackageReport, rpm_package: &str, executable: &str) -> String {
|
|
722
|
+
desktop_file(package, rpm_package, executable)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
fn desktop_file(package: &PackageReport, package_name: &str, executable: &str) -> String {
|
|
726
|
+
format!(
|
|
727
|
+
"[Desktop Entry]\n\
|
|
728
|
+
Name={name}\n\
|
|
729
|
+
Exec={executable} %U\n\
|
|
730
|
+
Terminal=false\n\
|
|
731
|
+
Type=Application\n\
|
|
732
|
+
StartupWMClass={wm_class}\n\
|
|
733
|
+
Categories=Utility;\n",
|
|
734
|
+
name = single_line(package.app_name()),
|
|
735
|
+
wm_class = package_name
|
|
736
|
+
)
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
fn gzip_tar(
|
|
740
|
+
write_contents: impl FnOnce(&mut TarBuilder<GzEncoder<Vec<u8>>>) -> Result<()>,
|
|
741
|
+
) -> Result<Vec<u8>> {
|
|
742
|
+
let encoder = GzEncoder::new(Vec::new(), Compression::default());
|
|
743
|
+
let mut builder = TarBuilder::new(encoder);
|
|
744
|
+
builder.mode(tar::HeaderMode::Deterministic);
|
|
745
|
+
write_contents(&mut builder)?;
|
|
746
|
+
builder.finish().context("Could not finish tar archive")?;
|
|
747
|
+
let encoder = builder
|
|
748
|
+
.into_inner()
|
|
749
|
+
.context("Could not retrieve gzip encoder")?;
|
|
750
|
+
encoder.finish().context("Could not finish gzip archive")
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
fn append_directory_contents_to_tar(
|
|
754
|
+
builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
|
|
755
|
+
source: &Path,
|
|
756
|
+
destination: &Path,
|
|
757
|
+
) -> Result<()> {
|
|
758
|
+
let mut entries = fs::read_dir(source)
|
|
759
|
+
.with_context(|| format!("Could not read {}", source.display()))?
|
|
760
|
+
.collect::<Result<Vec<_>, io::Error>>()?;
|
|
761
|
+
entries.sort_by_key(|entry| entry.path());
|
|
762
|
+
|
|
763
|
+
for entry in entries {
|
|
764
|
+
let source_path = entry.path();
|
|
765
|
+
let destination_path = destination.join(entry.file_name());
|
|
766
|
+
append_path_to_tar(builder, &source_path, &destination_path)?;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
Ok(())
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
fn append_path_to_tar(
|
|
773
|
+
builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
|
|
774
|
+
source: &Path,
|
|
775
|
+
destination: &Path,
|
|
776
|
+
) -> Result<()> {
|
|
777
|
+
let metadata = fs::symlink_metadata(source)
|
|
778
|
+
.with_context(|| format!("Could not stat {}", source.display()))?;
|
|
779
|
+
|
|
780
|
+
if metadata.is_dir() {
|
|
781
|
+
append_directory_to_tar(builder, destination, unix_mode(&metadata, 0o755))?;
|
|
782
|
+
append_directory_contents_to_tar(builder, source, destination)?;
|
|
783
|
+
} else if metadata.file_type().is_symlink() {
|
|
784
|
+
let target = fs::read_link(source)
|
|
785
|
+
.with_context(|| format!("Could not read link {}", source.display()))?;
|
|
786
|
+
append_symlink_to_tar(builder, destination, &target, 0o777)?;
|
|
787
|
+
} else if metadata.is_file() {
|
|
788
|
+
append_file_to_tar(builder, source, destination, &metadata)?;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
Ok(())
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
fn append_directory_to_tar(
|
|
795
|
+
builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
|
|
796
|
+
path: impl AsRef<Path>,
|
|
797
|
+
mode: u32,
|
|
798
|
+
) -> Result<()> {
|
|
799
|
+
let mut header = TarHeader::new_gnu();
|
|
800
|
+
header.set_entry_type(tar::EntryType::Directory);
|
|
801
|
+
header.set_size(0);
|
|
802
|
+
header.set_mode(mode);
|
|
803
|
+
header.set_mtime(0);
|
|
804
|
+
header.set_cksum();
|
|
805
|
+
builder
|
|
806
|
+
.append_data(&mut header, path.as_ref(), io::empty())
|
|
807
|
+
.with_context(|| format!("Could not add {} to data tar", path.as_ref().display()))
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
fn append_file_to_tar(
|
|
811
|
+
builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
|
|
812
|
+
source: &Path,
|
|
813
|
+
destination: &Path,
|
|
814
|
+
metadata: &fs::Metadata,
|
|
815
|
+
) -> Result<()> {
|
|
816
|
+
let mut header = TarHeader::new_gnu();
|
|
817
|
+
header.set_entry_type(tar::EntryType::Regular);
|
|
818
|
+
header.set_size(metadata.len());
|
|
819
|
+
header.set_mode(unix_mode(metadata, 0o644));
|
|
820
|
+
header.set_mtime(0);
|
|
821
|
+
header.set_cksum();
|
|
822
|
+
let mut file =
|
|
823
|
+
File::open(source).with_context(|| format!("Could not open {}", source.display()))?;
|
|
824
|
+
builder
|
|
825
|
+
.append_data(&mut header, destination, &mut file)
|
|
826
|
+
.with_context(|| format!("Could not add {} to data tar", source.display()))
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
fn append_bytes_to_tar(
|
|
830
|
+
builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
|
|
831
|
+
path: impl AsRef<Path>,
|
|
832
|
+
contents: &[u8],
|
|
833
|
+
mode: u32,
|
|
834
|
+
) -> Result<()> {
|
|
835
|
+
let mut header = TarHeader::new_gnu();
|
|
836
|
+
header.set_entry_type(tar::EntryType::Regular);
|
|
837
|
+
header.set_size(contents.len() as u64);
|
|
838
|
+
header.set_mode(mode);
|
|
839
|
+
header.set_mtime(0);
|
|
840
|
+
header.set_cksum();
|
|
841
|
+
builder
|
|
842
|
+
.append_data(&mut header, path.as_ref(), contents)
|
|
843
|
+
.with_context(|| format!("Could not add {} to tar", path.as_ref().display()))
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
fn append_symlink_to_tar(
|
|
847
|
+
builder: &mut TarBuilder<GzEncoder<Vec<u8>>>,
|
|
848
|
+
path: impl AsRef<Path>,
|
|
849
|
+
target: impl AsRef<Path>,
|
|
850
|
+
mode: u32,
|
|
851
|
+
) -> Result<()> {
|
|
852
|
+
let mut header = TarHeader::new_gnu();
|
|
853
|
+
header.set_entry_type(tar::EntryType::Symlink);
|
|
854
|
+
header.set_size(0);
|
|
855
|
+
header.set_mode(mode);
|
|
856
|
+
header.set_mtime(0);
|
|
857
|
+
header
|
|
858
|
+
.set_link_name(target.as_ref())
|
|
859
|
+
.with_context(|| format!("Could not set link target for {}", path.as_ref().display()))?;
|
|
860
|
+
header.set_cksum();
|
|
861
|
+
builder
|
|
862
|
+
.append_data(&mut header, path.as_ref(), io::empty())
|
|
863
|
+
.with_context(|| format!("Could not add {} to tar", path.as_ref().display()))
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
struct ArMember {
|
|
867
|
+
name: &'static str,
|
|
868
|
+
mode: u32,
|
|
869
|
+
data: Vec<u8>,
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
fn write_ar_archive(artifact: &Path, members: &[ArMember]) -> Result<()> {
|
|
873
|
+
let mut file = BufWriter::new(
|
|
874
|
+
File::create(artifact)
|
|
875
|
+
.with_context(|| format!("Could not create {}", artifact.display()))?,
|
|
876
|
+
);
|
|
877
|
+
file.write_all(b"!<arch>\n")
|
|
878
|
+
.with_context(|| format!("Could not write {}", artifact.display()))?;
|
|
879
|
+
|
|
880
|
+
for member in members {
|
|
881
|
+
write_ar_member(&mut file, member)
|
|
882
|
+
.with_context(|| format!("Could not add {} to {}", member.name, artifact.display()))?;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
file.flush()
|
|
886
|
+
.with_context(|| format!("Could not finish {}", artifact.display()))
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
fn write_ar_member(writer: &mut impl Write, member: &ArMember) -> Result<()> {
|
|
890
|
+
let name = format!("{}/", member.name);
|
|
891
|
+
if name.len() > 16 {
|
|
892
|
+
bail!("ar member name is too long: {}", member.name);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
let header = format!(
|
|
896
|
+
"{name:<16}{mtime:<12}{uid:<6}{gid:<6}{mode:<8o}{size:<10}`\n",
|
|
897
|
+
mtime = 0,
|
|
898
|
+
uid = 0,
|
|
899
|
+
gid = 0,
|
|
900
|
+
mode = member.mode,
|
|
901
|
+
size = member.data.len()
|
|
902
|
+
);
|
|
903
|
+
debug_assert_eq!(header.len(), 60);
|
|
904
|
+
writer.write_all(header.as_bytes())?;
|
|
905
|
+
writer.write_all(&member.data)?;
|
|
906
|
+
if member.data.len() % 2 == 1 {
|
|
907
|
+
writer.write_all(b"\n")?;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
Ok(())
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
fn directory_size(path: &Path) -> Result<u64> {
|
|
914
|
+
let metadata =
|
|
915
|
+
fs::symlink_metadata(path).with_context(|| format!("Could not stat {}", path.display()))?;
|
|
916
|
+
if metadata.is_file() {
|
|
917
|
+
return Ok(metadata.len());
|
|
918
|
+
}
|
|
919
|
+
if !metadata.is_dir() {
|
|
920
|
+
return Ok(0);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
let mut size = 0;
|
|
924
|
+
for entry in fs::read_dir(path).with_context(|| format!("Could not read {}", path.display()))? {
|
|
925
|
+
let entry = entry?;
|
|
926
|
+
size += directory_size(&entry.path())?;
|
|
927
|
+
}
|
|
928
|
+
Ok(size)
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
fn debian_package_name(name: &str) -> String {
|
|
932
|
+
package_name(name)
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
fn rpm_package_name(name: &str) -> String {
|
|
936
|
+
package_name(name)
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
fn package_name(name: &str) -> String {
|
|
940
|
+
let mut package = name
|
|
941
|
+
.to_ascii_lowercase()
|
|
942
|
+
.chars()
|
|
943
|
+
.map(|char| {
|
|
944
|
+
if char.is_ascii_alphanumeric() || matches!(char, '+' | '-' | '.') {
|
|
945
|
+
char
|
|
946
|
+
} else {
|
|
947
|
+
'-'
|
|
948
|
+
}
|
|
949
|
+
})
|
|
950
|
+
.collect::<String>()
|
|
951
|
+
.trim_matches(['+', '-', '.'])
|
|
952
|
+
.to_string();
|
|
953
|
+
|
|
954
|
+
if package.len() < 2 {
|
|
955
|
+
package.push_str("app");
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
package
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
fn debian_version(version: Option<&str>) -> String {
|
|
962
|
+
let version = version.unwrap_or("0.1.0");
|
|
963
|
+
let sanitized = version
|
|
964
|
+
.chars()
|
|
965
|
+
.map(|char| {
|
|
966
|
+
if char.is_ascii_alphanumeric() || matches!(char, '.' | '+' | '-' | ':' | '~') {
|
|
967
|
+
char
|
|
968
|
+
} else {
|
|
969
|
+
'~'
|
|
970
|
+
}
|
|
971
|
+
})
|
|
972
|
+
.collect::<String>()
|
|
973
|
+
.trim_matches(['-', '~'])
|
|
974
|
+
.to_string();
|
|
975
|
+
|
|
976
|
+
if sanitized.is_empty() {
|
|
977
|
+
"0.1.0".to_string()
|
|
978
|
+
} else {
|
|
979
|
+
sanitized
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
fn dmg_version(version: Option<&str>) -> String {
|
|
984
|
+
let version = version.unwrap_or("0.1.0");
|
|
985
|
+
let sanitized = version
|
|
986
|
+
.chars()
|
|
987
|
+
.map(|char| {
|
|
988
|
+
if char.is_ascii_alphanumeric() || matches!(char, '.' | '-' | '_') {
|
|
989
|
+
char
|
|
990
|
+
} else {
|
|
991
|
+
'-'
|
|
992
|
+
}
|
|
993
|
+
})
|
|
994
|
+
.collect::<String>()
|
|
995
|
+
.trim_matches(['-', '.', '_'])
|
|
996
|
+
.to_string();
|
|
997
|
+
|
|
998
|
+
if sanitized.is_empty() {
|
|
999
|
+
"0.1.0".to_string()
|
|
1000
|
+
} else {
|
|
1001
|
+
sanitized
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
fn rpm_version(version: Option<&str>) -> String {
|
|
1006
|
+
let version = version.unwrap_or("0.1.0");
|
|
1007
|
+
let sanitized = version
|
|
1008
|
+
.chars()
|
|
1009
|
+
.map(|char| {
|
|
1010
|
+
if char.is_ascii_alphanumeric() || matches!(char, '.' | '+' | '_' | '~') {
|
|
1011
|
+
char
|
|
1012
|
+
} else {
|
|
1013
|
+
'_'
|
|
1014
|
+
}
|
|
1015
|
+
})
|
|
1016
|
+
.collect::<String>()
|
|
1017
|
+
.trim_matches(['_', '~'])
|
|
1018
|
+
.to_string();
|
|
1019
|
+
|
|
1020
|
+
if sanitized.is_empty() {
|
|
1021
|
+
"0.1.0".to_string()
|
|
1022
|
+
} else {
|
|
1023
|
+
sanitized
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
fn debian_arch(arch: &str) -> String {
|
|
1028
|
+
match arch {
|
|
1029
|
+
"x64" => "amd64".to_string(),
|
|
1030
|
+
"ia32" => "i386".to_string(),
|
|
1031
|
+
"armv7l" => "armhf".to_string(),
|
|
1032
|
+
arch => arch.to_string(),
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
fn rpm_arch(arch: &str) -> String {
|
|
1037
|
+
match arch {
|
|
1038
|
+
"x64" => "x86_64".to_string(),
|
|
1039
|
+
"arm64" => "aarch64".to_string(),
|
|
1040
|
+
"ia32" => "i386".to_string(),
|
|
1041
|
+
"armv7l" => "armv7hl".to_string(),
|
|
1042
|
+
arch => arch.to_string(),
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
fn single_line(value: &str) -> String {
|
|
1047
|
+
value
|
|
1048
|
+
.chars()
|
|
1049
|
+
.map(|char| {
|
|
1050
|
+
if char == '\n' || char == '\r' {
|
|
1051
|
+
' '
|
|
1052
|
+
} else {
|
|
1053
|
+
char
|
|
1054
|
+
}
|
|
1055
|
+
})
|
|
1056
|
+
.collect()
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
#[cfg(unix)]
|
|
1060
|
+
fn unix_mode(metadata: &fs::Metadata, _fallback: u32) -> u32 {
|
|
1061
|
+
use std::os::unix::fs::PermissionsExt;
|
|
1062
|
+
|
|
1063
|
+
metadata.permissions().mode()
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
#[cfg(not(unix))]
|
|
1067
|
+
fn unix_mode(_metadata: &fs::Metadata, fallback: u32) -> u32 {
|
|
1068
|
+
fallback
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
fn utf8_path(path: PathBuf) -> Result<Utf8PathBuf> {
|
|
1072
|
+
Utf8PathBuf::from_path_buf(path).map_err(|path| {
|
|
1073
|
+
anyhow::anyhow!(
|
|
1074
|
+
"Path contains invalid UTF-8 and cannot be represented in JSON: {}",
|
|
1075
|
+
path.display()
|
|
1076
|
+
)
|
|
1077
|
+
})
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
impl MakeStatus {
|
|
1081
|
+
fn as_str(&self) -> &'static str {
|
|
1082
|
+
match self {
|
|
1083
|
+
MakeStatus::Planned => "planned",
|
|
1084
|
+
MakeStatus::Made => "made",
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
impl MakeReport {
|
|
1090
|
+
pub(crate) fn mark_made(&mut self) -> Result<()> {
|
|
1091
|
+
self.status = MakeStatus::Made;
|
|
1092
|
+
self.artifact_size = Some(
|
|
1093
|
+
fs::metadata(self.artifact.as_str())
|
|
1094
|
+
.with_context(|| format!("Could not stat {}", self.artifact))?
|
|
1095
|
+
.len(),
|
|
1096
|
+
);
|
|
1097
|
+
Ok(())
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
pub(crate) fn package(&self) -> &PackageReport {
|
|
1101
|
+
&self.package
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
pub(crate) fn target(&self) -> &str {
|
|
1105
|
+
&self.target
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
pub(crate) fn artifact(&self) -> &Utf8PathBuf {
|
|
1109
|
+
&self.artifact
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
pub(crate) fn warnings(&self) -> &[String] {
|
|
1113
|
+
&self.warnings
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
#[cfg(test)]
|
|
1118
|
+
mod tests {
|
|
1119
|
+
use super::*;
|
|
1120
|
+
use std::io::Read;
|
|
1121
|
+
use zip::ZipArchive;
|
|
1122
|
+
|
|
1123
|
+
#[test]
|
|
1124
|
+
fn builds_make_report_for_zip_target() {
|
|
1125
|
+
let root = unique_temp_dir("plan");
|
|
1126
|
+
write_package_json(&root);
|
|
1127
|
+
write_app_file(&root);
|
|
1128
|
+
write_fake_electron_dist(&root);
|
|
1129
|
+
|
|
1130
|
+
let args = MakeArgs {
|
|
1131
|
+
cwd: root.clone(),
|
|
1132
|
+
out_dir: PathBuf::from("out"),
|
|
1133
|
+
name: None,
|
|
1134
|
+
platform: None,
|
|
1135
|
+
arch: None,
|
|
1136
|
+
target: crate::cli::MakeTarget::Zip,
|
|
1137
|
+
skip_package: false,
|
|
1138
|
+
force: false,
|
|
1139
|
+
dry_run: true,
|
|
1140
|
+
json: true,
|
|
1141
|
+
};
|
|
1142
|
+
let report = build_report(&args).expect("report should build");
|
|
1143
|
+
|
|
1144
|
+
assert_eq!(report.target, "zip");
|
|
1145
|
+
let expected_suffix = PathBuf::from("out")
|
|
1146
|
+
.join("make")
|
|
1147
|
+
.join("zip")
|
|
1148
|
+
.join(report.package.platform())
|
|
1149
|
+
.join(report.package.arch())
|
|
1150
|
+
.join(format!(
|
|
1151
|
+
"starter-app-{}-{}.zip",
|
|
1152
|
+
report.package.platform(),
|
|
1153
|
+
report.package.arch()
|
|
1154
|
+
));
|
|
1155
|
+
assert!(Path::new(report.artifact.as_str()).ends_with(expected_suffix));
|
|
1156
|
+
|
|
1157
|
+
let _ = fs::remove_dir_all(root);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
#[test]
|
|
1161
|
+
fn builds_make_report_for_deb_target() {
|
|
1162
|
+
let root = unique_temp_dir("deb-plan");
|
|
1163
|
+
write_package_json(&root);
|
|
1164
|
+
write_app_file(&root);
|
|
1165
|
+
write_fake_electron_dist(&root);
|
|
1166
|
+
|
|
1167
|
+
let args = MakeArgs {
|
|
1168
|
+
cwd: root.clone(),
|
|
1169
|
+
out_dir: PathBuf::from("out"),
|
|
1170
|
+
name: None,
|
|
1171
|
+
platform: Some("linux".to_string()),
|
|
1172
|
+
arch: Some("x64".to_string()),
|
|
1173
|
+
target: crate::cli::MakeTarget::Deb,
|
|
1174
|
+
skip_package: false,
|
|
1175
|
+
force: false,
|
|
1176
|
+
dry_run: true,
|
|
1177
|
+
json: true,
|
|
1178
|
+
};
|
|
1179
|
+
let report = build_report(&args).expect("report should build");
|
|
1180
|
+
|
|
1181
|
+
assert_eq!(report.target, "deb");
|
|
1182
|
+
assert!(Path::new(report.artifact.as_str()).ends_with(
|
|
1183
|
+
PathBuf::from("out")
|
|
1184
|
+
.join("make")
|
|
1185
|
+
.join("deb")
|
|
1186
|
+
.join("linux")
|
|
1187
|
+
.join("x64")
|
|
1188
|
+
.join("starter-app_0.1.0_amd64.deb")
|
|
1189
|
+
));
|
|
1190
|
+
|
|
1191
|
+
let _ = fs::remove_dir_all(root);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
#[test]
|
|
1195
|
+
fn builds_make_report_for_dmg_target() {
|
|
1196
|
+
let root = unique_temp_dir("dmg-plan");
|
|
1197
|
+
write_package_json(&root);
|
|
1198
|
+
write_app_file(&root);
|
|
1199
|
+
write_fake_electron_dist(&root);
|
|
1200
|
+
|
|
1201
|
+
let args = MakeArgs {
|
|
1202
|
+
cwd: root.clone(),
|
|
1203
|
+
out_dir: PathBuf::from("out"),
|
|
1204
|
+
name: None,
|
|
1205
|
+
platform: Some("darwin".to_string()),
|
|
1206
|
+
arch: Some("arm64".to_string()),
|
|
1207
|
+
target: crate::cli::MakeTarget::Dmg,
|
|
1208
|
+
skip_package: false,
|
|
1209
|
+
force: false,
|
|
1210
|
+
dry_run: true,
|
|
1211
|
+
json: true,
|
|
1212
|
+
};
|
|
1213
|
+
let report = build_report(&args).expect("report should build");
|
|
1214
|
+
|
|
1215
|
+
assert_eq!(report.target, "dmg");
|
|
1216
|
+
assert!(Path::new(report.artifact.as_str()).ends_with(
|
|
1217
|
+
PathBuf::from("out")
|
|
1218
|
+
.join("make")
|
|
1219
|
+
.join("dmg")
|
|
1220
|
+
.join("darwin")
|
|
1221
|
+
.join("arm64")
|
|
1222
|
+
.join("starter-app-0.1.0-arm64.dmg")
|
|
1223
|
+
));
|
|
1224
|
+
|
|
1225
|
+
let _ = fs::remove_dir_all(root);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
#[test]
|
|
1229
|
+
fn builds_make_report_for_rpm_target() {
|
|
1230
|
+
let root = unique_temp_dir("rpm-plan");
|
|
1231
|
+
write_package_json(&root);
|
|
1232
|
+
write_app_file(&root);
|
|
1233
|
+
write_fake_electron_dist(&root);
|
|
1234
|
+
|
|
1235
|
+
let args = MakeArgs {
|
|
1236
|
+
cwd: root.clone(),
|
|
1237
|
+
out_dir: PathBuf::from("out"),
|
|
1238
|
+
name: None,
|
|
1239
|
+
platform: Some("linux".to_string()),
|
|
1240
|
+
arch: Some("x64".to_string()),
|
|
1241
|
+
target: crate::cli::MakeTarget::Rpm,
|
|
1242
|
+
skip_package: false,
|
|
1243
|
+
force: false,
|
|
1244
|
+
dry_run: true,
|
|
1245
|
+
json: true,
|
|
1246
|
+
};
|
|
1247
|
+
let report = build_report(&args).expect("report should build");
|
|
1248
|
+
|
|
1249
|
+
assert_eq!(report.target, "rpm");
|
|
1250
|
+
assert!(Path::new(report.artifact.as_str()).ends_with(
|
|
1251
|
+
PathBuf::from("out")
|
|
1252
|
+
.join("make")
|
|
1253
|
+
.join("rpm")
|
|
1254
|
+
.join("linux")
|
|
1255
|
+
.join("x64")
|
|
1256
|
+
.join("starter-app-0.1.0-1.x86_64.rpm")
|
|
1257
|
+
));
|
|
1258
|
+
|
|
1259
|
+
let _ = fs::remove_dir_all(root);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
#[test]
|
|
1263
|
+
fn makes_zip_artifact_after_packaging() {
|
|
1264
|
+
let root = unique_temp_dir("execute");
|
|
1265
|
+
write_package_json(&root);
|
|
1266
|
+
write_app_file(&root);
|
|
1267
|
+
write_fake_electron_dist(&root);
|
|
1268
|
+
|
|
1269
|
+
let args = MakeArgs {
|
|
1270
|
+
cwd: root.clone(),
|
|
1271
|
+
out_dir: PathBuf::from("out"),
|
|
1272
|
+
name: None,
|
|
1273
|
+
platform: None,
|
|
1274
|
+
arch: None,
|
|
1275
|
+
target: crate::cli::MakeTarget::Zip,
|
|
1276
|
+
skip_package: false,
|
|
1277
|
+
force: false,
|
|
1278
|
+
dry_run: false,
|
|
1279
|
+
json: false,
|
|
1280
|
+
};
|
|
1281
|
+
let mut report = build_report(&args).expect("report should build");
|
|
1282
|
+
|
|
1283
|
+
execute_make(&mut report, &args).expect("make should succeed");
|
|
1284
|
+
|
|
1285
|
+
let file = File::open(report.artifact.as_str()).expect("artifact should exist");
|
|
1286
|
+
let mut archive = ZipArchive::new(file).expect("zip should open");
|
|
1287
|
+
let app_entry = if report.package.platform() == "darwin" {
|
|
1288
|
+
"starter-app.app/Contents/Resources/app/package.json".to_string()
|
|
1289
|
+
} else {
|
|
1290
|
+
format!(
|
|
1291
|
+
"starter-app-{}-{}/resources/app/package.json",
|
|
1292
|
+
report.package.platform(),
|
|
1293
|
+
report.package.arch()
|
|
1294
|
+
)
|
|
1295
|
+
};
|
|
1296
|
+
|
|
1297
|
+
archive
|
|
1298
|
+
.by_name(&app_entry)
|
|
1299
|
+
.expect("app package.json should be archived");
|
|
1300
|
+
|
|
1301
|
+
let _ = fs::remove_dir_all(root);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
#[test]
|
|
1305
|
+
fn writes_deb_archive_with_control_and_data_members() {
|
|
1306
|
+
let root = unique_temp_dir("deb-archive");
|
|
1307
|
+
write_package_json(&root);
|
|
1308
|
+
write_app_file(&root);
|
|
1309
|
+
write_fake_electron_dist(&root);
|
|
1310
|
+
|
|
1311
|
+
let args = MakeArgs {
|
|
1312
|
+
cwd: root.clone(),
|
|
1313
|
+
out_dir: PathBuf::from("out"),
|
|
1314
|
+
name: None,
|
|
1315
|
+
platform: Some("linux".to_string()),
|
|
1316
|
+
arch: Some("x64".to_string()),
|
|
1317
|
+
target: crate::cli::MakeTarget::Deb,
|
|
1318
|
+
skip_package: false,
|
|
1319
|
+
force: false,
|
|
1320
|
+
dry_run: true,
|
|
1321
|
+
json: true,
|
|
1322
|
+
};
|
|
1323
|
+
let report = build_report(&args).expect("report should build");
|
|
1324
|
+
let bundle_dir = Path::new(report.package.bundle_dir().as_str());
|
|
1325
|
+
fs::create_dir_all(bundle_dir.join("resources/app"))
|
|
1326
|
+
.expect("fake bundle resources should be created");
|
|
1327
|
+
fs::write(bundle_dir.join("starter-app"), "").expect("fake binary should be written");
|
|
1328
|
+
fs::write(bundle_dir.join("resources/app/package.json"), "{}")
|
|
1329
|
+
.expect("fake app package should be written");
|
|
1330
|
+
|
|
1331
|
+
write_deb_archive(&report.package, Path::new(report.artifact.as_str()))
|
|
1332
|
+
.expect("deb should be written");
|
|
1333
|
+
|
|
1334
|
+
let members = read_ar_members(Path::new(report.artifact.as_str()));
|
|
1335
|
+
assert_eq!(
|
|
1336
|
+
members.get("debian-binary").map(Vec::as_slice),
|
|
1337
|
+
Some(&b"2.0\n"[..])
|
|
1338
|
+
);
|
|
1339
|
+
|
|
1340
|
+
let control = read_tar_file(
|
|
1341
|
+
members
|
|
1342
|
+
.get("control.tar.gz")
|
|
1343
|
+
.expect("control tar should exist"),
|
|
1344
|
+
"control",
|
|
1345
|
+
);
|
|
1346
|
+
assert!(control.contains("Package: starter-app"));
|
|
1347
|
+
assert!(control.contains("Architecture: amd64"));
|
|
1348
|
+
|
|
1349
|
+
let data = members.get("data.tar.gz").expect("data tar should exist");
|
|
1350
|
+
assert!(tar_contains(
|
|
1351
|
+
data,
|
|
1352
|
+
"opt/starter-app/resources/app/package.json"
|
|
1353
|
+
));
|
|
1354
|
+
assert!(tar_contains(
|
|
1355
|
+
data,
|
|
1356
|
+
"usr/share/applications/starter-app.desktop"
|
|
1357
|
+
));
|
|
1358
|
+
assert!(tar_contains(data, "usr/bin/starter-app"));
|
|
1359
|
+
|
|
1360
|
+
let _ = fs::remove_dir_all(root);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
#[test]
|
|
1364
|
+
fn writes_dmg_archive_with_app_bundle_and_applications_entry() {
|
|
1365
|
+
let root = unique_temp_dir("dmg-archive");
|
|
1366
|
+
write_package_json(&root);
|
|
1367
|
+
write_app_file(&root);
|
|
1368
|
+
write_fake_electron_dist(&root);
|
|
1369
|
+
|
|
1370
|
+
let args = MakeArgs {
|
|
1371
|
+
cwd: root.clone(),
|
|
1372
|
+
out_dir: PathBuf::from("out"),
|
|
1373
|
+
name: None,
|
|
1374
|
+
platform: Some("darwin".to_string()),
|
|
1375
|
+
arch: Some("arm64".to_string()),
|
|
1376
|
+
target: crate::cli::MakeTarget::Dmg,
|
|
1377
|
+
skip_package: false,
|
|
1378
|
+
force: false,
|
|
1379
|
+
dry_run: true,
|
|
1380
|
+
json: true,
|
|
1381
|
+
};
|
|
1382
|
+
let report = build_report(&args).expect("report should build");
|
|
1383
|
+
write_fake_macos_bundle(
|
|
1384
|
+
Path::new(report.package.bundle_dir().as_str()),
|
|
1385
|
+
"starter-app",
|
|
1386
|
+
);
|
|
1387
|
+
|
|
1388
|
+
write_dmg_archive(&report.package, Path::new(report.artifact.as_str()))
|
|
1389
|
+
.expect("dmg should be written");
|
|
1390
|
+
|
|
1391
|
+
let mut dmg = apple_dmg::DmgReader::open(Path::new(report.artifact.as_str()))
|
|
1392
|
+
.expect("dmg should parse");
|
|
1393
|
+
assert_eq!(dmg.plist().partitions().len(), 2);
|
|
1394
|
+
let fat32 = dmg.partition_data(1).expect("fat32 partition should read");
|
|
1395
|
+
let fs = fatfs::FileSystem::new(Cursor::new(fat32), fatfs::FsOptions::new())
|
|
1396
|
+
.expect("fat32 should mount");
|
|
1397
|
+
let root_dir = fs.root_dir();
|
|
1398
|
+
let entries = root_dir
|
|
1399
|
+
.iter()
|
|
1400
|
+
.map(|entry| entry.expect("fat entry should read").file_name())
|
|
1401
|
+
.collect::<Vec<_>>();
|
|
1402
|
+
assert!(entries.contains(&"starter-app.app".to_string()));
|
|
1403
|
+
assert!(entries.contains(&"Applications".to_string()));
|
|
1404
|
+
|
|
1405
|
+
let app_dir = root_dir
|
|
1406
|
+
.open_dir("starter-app.app")
|
|
1407
|
+
.expect("app bundle should exist");
|
|
1408
|
+
let contents = app_dir.open_dir("Contents").expect("Contents should exist");
|
|
1409
|
+
let resources = contents
|
|
1410
|
+
.open_dir("Resources")
|
|
1411
|
+
.expect("Resources should exist");
|
|
1412
|
+
let app_resources = resources
|
|
1413
|
+
.open_dir("app")
|
|
1414
|
+
.expect("app resources should exist");
|
|
1415
|
+
app_resources
|
|
1416
|
+
.open_file("package.json")
|
|
1417
|
+
.expect("app package should exist");
|
|
1418
|
+
|
|
1419
|
+
let mut applications = String::new();
|
|
1420
|
+
root_dir
|
|
1421
|
+
.open_file("Applications")
|
|
1422
|
+
.expect("Applications entry should exist")
|
|
1423
|
+
.read_to_string(&mut applications)
|
|
1424
|
+
.expect("Applications entry should read");
|
|
1425
|
+
assert!(applications.starts_with("XSym\n0013\n"));
|
|
1426
|
+
assert!(applications.contains("/Applications"));
|
|
1427
|
+
|
|
1428
|
+
let _ = fs::remove_dir_all(root);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
#[test]
|
|
1432
|
+
fn writes_rpm_archive_with_metadata_and_payload_entries() {
|
|
1433
|
+
let root = unique_temp_dir("rpm-archive");
|
|
1434
|
+
write_package_json(&root);
|
|
1435
|
+
write_app_file(&root);
|
|
1436
|
+
write_fake_electron_dist(&root);
|
|
1437
|
+
|
|
1438
|
+
let args = MakeArgs {
|
|
1439
|
+
cwd: root.clone(),
|
|
1440
|
+
out_dir: PathBuf::from("out"),
|
|
1441
|
+
name: None,
|
|
1442
|
+
platform: Some("linux".to_string()),
|
|
1443
|
+
arch: Some("x64".to_string()),
|
|
1444
|
+
target: crate::cli::MakeTarget::Rpm,
|
|
1445
|
+
skip_package: false,
|
|
1446
|
+
force: false,
|
|
1447
|
+
dry_run: true,
|
|
1448
|
+
json: true,
|
|
1449
|
+
};
|
|
1450
|
+
let report = build_report(&args).expect("report should build");
|
|
1451
|
+
let bundle_dir = Path::new(report.package.bundle_dir().as_str());
|
|
1452
|
+
fs::create_dir_all(bundle_dir.join("resources/app"))
|
|
1453
|
+
.expect("fake bundle resources should be created");
|
|
1454
|
+
fs::write(bundle_dir.join("starter-app"), "").expect("fake binary should be written");
|
|
1455
|
+
fs::write(bundle_dir.join("resources/app/package.json"), "{}")
|
|
1456
|
+
.expect("fake app package should be written");
|
|
1457
|
+
|
|
1458
|
+
write_rpm_archive(&report.package, Path::new(report.artifact.as_str()))
|
|
1459
|
+
.expect("rpm should be written");
|
|
1460
|
+
|
|
1461
|
+
let rpm = rpm::Package::open(report.artifact.as_str()).expect("rpm should parse");
|
|
1462
|
+
assert_eq!(
|
|
1463
|
+
rpm.metadata.get_name().expect("name should read"),
|
|
1464
|
+
"starter-app"
|
|
1465
|
+
);
|
|
1466
|
+
assert_eq!(
|
|
1467
|
+
rpm.metadata.get_version().expect("version should read"),
|
|
1468
|
+
"0.1.0"
|
|
1469
|
+
);
|
|
1470
|
+
assert_eq!(
|
|
1471
|
+
rpm.metadata.get_release().expect("release should read"),
|
|
1472
|
+
"1"
|
|
1473
|
+
);
|
|
1474
|
+
assert_eq!(rpm.metadata.get_arch().expect("arch should read"), "x86_64");
|
|
1475
|
+
|
|
1476
|
+
let paths = rpm
|
|
1477
|
+
.metadata
|
|
1478
|
+
.get_file_paths()
|
|
1479
|
+
.expect("file paths should read")
|
|
1480
|
+
.into_iter()
|
|
1481
|
+
.map(|path| path.to_string_lossy().to_string())
|
|
1482
|
+
.collect::<Vec<_>>();
|
|
1483
|
+
assert!(paths.contains(&"/opt/starter-app/resources/app/package.json".to_string()));
|
|
1484
|
+
assert!(paths.contains(&"/usr/share/applications/starter-app.desktop".to_string()));
|
|
1485
|
+
assert!(paths.contains(&"/usr/bin/starter-app".to_string()));
|
|
1486
|
+
|
|
1487
|
+
let _ = fs::remove_dir_all(root);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
#[test]
|
|
1491
|
+
fn makes_deb_artifact_after_packaging_on_linux() {
|
|
1492
|
+
if !cfg!(target_os = "linux") {
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
let root = unique_temp_dir("deb-execute");
|
|
1497
|
+
write_package_json(&root);
|
|
1498
|
+
write_app_file(&root);
|
|
1499
|
+
write_fake_electron_dist(&root);
|
|
1500
|
+
|
|
1501
|
+
let args = MakeArgs {
|
|
1502
|
+
cwd: root.clone(),
|
|
1503
|
+
out_dir: PathBuf::from("out"),
|
|
1504
|
+
name: None,
|
|
1505
|
+
platform: None,
|
|
1506
|
+
arch: None,
|
|
1507
|
+
target: crate::cli::MakeTarget::Deb,
|
|
1508
|
+
skip_package: false,
|
|
1509
|
+
force: false,
|
|
1510
|
+
dry_run: false,
|
|
1511
|
+
json: false,
|
|
1512
|
+
};
|
|
1513
|
+
let mut report = build_report(&args).expect("report should build");
|
|
1514
|
+
|
|
1515
|
+
execute_make(&mut report, &args).expect("make should succeed");
|
|
1516
|
+
|
|
1517
|
+
assert!(Path::new(report.artifact.as_str()).exists());
|
|
1518
|
+
|
|
1519
|
+
let _ = fs::remove_dir_all(root);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
#[test]
|
|
1523
|
+
fn makes_dmg_artifact_after_packaging_on_macos() {
|
|
1524
|
+
if !cfg!(target_os = "macos") {
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
let root = unique_temp_dir("dmg-execute");
|
|
1529
|
+
write_package_json(&root);
|
|
1530
|
+
write_app_file(&root);
|
|
1531
|
+
write_fake_electron_dist(&root);
|
|
1532
|
+
|
|
1533
|
+
let args = MakeArgs {
|
|
1534
|
+
cwd: root.clone(),
|
|
1535
|
+
out_dir: PathBuf::from("out"),
|
|
1536
|
+
name: None,
|
|
1537
|
+
platform: None,
|
|
1538
|
+
arch: None,
|
|
1539
|
+
target: crate::cli::MakeTarget::Dmg,
|
|
1540
|
+
skip_package: false,
|
|
1541
|
+
force: false,
|
|
1542
|
+
dry_run: false,
|
|
1543
|
+
json: false,
|
|
1544
|
+
};
|
|
1545
|
+
let mut report = build_report(&args).expect("report should build");
|
|
1546
|
+
|
|
1547
|
+
execute_make(&mut report, &args).expect("make should succeed");
|
|
1548
|
+
|
|
1549
|
+
assert!(Path::new(report.artifact.as_str()).exists());
|
|
1550
|
+
|
|
1551
|
+
let _ = fs::remove_dir_all(root);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
#[test]
|
|
1555
|
+
fn makes_rpm_artifact_after_packaging_on_linux() {
|
|
1556
|
+
if !cfg!(target_os = "linux") {
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
let root = unique_temp_dir("rpm-execute");
|
|
1561
|
+
write_package_json(&root);
|
|
1562
|
+
write_app_file(&root);
|
|
1563
|
+
write_fake_electron_dist(&root);
|
|
1564
|
+
|
|
1565
|
+
let args = MakeArgs {
|
|
1566
|
+
cwd: root.clone(),
|
|
1567
|
+
out_dir: PathBuf::from("out"),
|
|
1568
|
+
name: None,
|
|
1569
|
+
platform: None,
|
|
1570
|
+
arch: None,
|
|
1571
|
+
target: crate::cli::MakeTarget::Rpm,
|
|
1572
|
+
skip_package: false,
|
|
1573
|
+
force: false,
|
|
1574
|
+
dry_run: false,
|
|
1575
|
+
json: false,
|
|
1576
|
+
};
|
|
1577
|
+
let mut report = build_report(&args).expect("report should build");
|
|
1578
|
+
|
|
1579
|
+
execute_make(&mut report, &args).expect("make should succeed");
|
|
1580
|
+
|
|
1581
|
+
assert!(Path::new(report.artifact.as_str()).exists());
|
|
1582
|
+
|
|
1583
|
+
let _ = fs::remove_dir_all(root);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
fn write_package_json(root: &Path) {
|
|
1587
|
+
fs::write(
|
|
1588
|
+
root.join("package.json"),
|
|
1589
|
+
r#"{"name":"starter-app","version":"0.1.0","license":"MIT","main":"src/main.js","devDependencies":{"electron":"30.0.0"}}"#,
|
|
1590
|
+
)
|
|
1591
|
+
.expect("package.json should be written");
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
fn write_app_file(root: &Path) {
|
|
1595
|
+
fs::create_dir_all(root.join("src")).expect("src should be created");
|
|
1596
|
+
fs::write(root.join("src/main.js"), "console.log('hello');")
|
|
1597
|
+
.expect("main file should be written");
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
fn write_fake_macos_bundle(bundle_dir: &Path, executable_name: &str) {
|
|
1601
|
+
fs::create_dir_all(bundle_dir.join("Contents/MacOS"))
|
|
1602
|
+
.expect("fake macOS executable directory should be created");
|
|
1603
|
+
fs::create_dir_all(bundle_dir.join("Contents/Resources/app"))
|
|
1604
|
+
.expect("fake macOS resources should be created");
|
|
1605
|
+
fs::write(
|
|
1606
|
+
bundle_dir.join("Contents/MacOS").join(executable_name),
|
|
1607
|
+
"#!/bin/sh\n",
|
|
1608
|
+
)
|
|
1609
|
+
.expect("fake macOS executable should be written");
|
|
1610
|
+
fs::write(bundle_dir.join("Contents/Info.plist"), "<plist/>")
|
|
1611
|
+
.expect("fake macOS plist should be written");
|
|
1612
|
+
fs::write(bundle_dir.join("Contents/Resources/app/package.json"), "{}")
|
|
1613
|
+
.expect("fake app package should be written");
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
fn write_fake_electron_dist(root: &Path) {
|
|
1617
|
+
let dist = root.join("node_modules/electron/dist");
|
|
1618
|
+
if cfg!(target_os = "macos") {
|
|
1619
|
+
let app = dist.join("Electron.app/Contents/MacOS");
|
|
1620
|
+
fs::create_dir_all(&app).expect("fake macOS electron app should be created");
|
|
1621
|
+
fs::write(app.join("Electron"), "").expect("fake macOS binary should be written");
|
|
1622
|
+
} else if cfg!(target_os = "windows") {
|
|
1623
|
+
fs::create_dir_all(&dist).expect("fake electron dist should be created");
|
|
1624
|
+
fs::write(dist.join("electron.exe"), "").expect("fake exe should be written");
|
|
1625
|
+
} else {
|
|
1626
|
+
fs::create_dir_all(&dist).expect("fake electron dist should be created");
|
|
1627
|
+
fs::write(dist.join("electron"), "").expect("fake binary should be written");
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
fn unique_temp_dir(label: &str) -> PathBuf {
|
|
1632
|
+
let nanos = std::time::SystemTime::now()
|
|
1633
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
1634
|
+
.expect("clock should be after epoch")
|
|
1635
|
+
.as_nanos();
|
|
1636
|
+
let path = std::env::temp_dir().join(format!(
|
|
1637
|
+
"electron-cli-make-{label}-{}-{nanos}",
|
|
1638
|
+
std::process::id()
|
|
1639
|
+
));
|
|
1640
|
+
fs::create_dir_all(&path).expect("temp dir should be created");
|
|
1641
|
+
path
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
fn read_ar_members(path: &Path) -> std::collections::BTreeMap<String, Vec<u8>> {
|
|
1645
|
+
let bytes = fs::read(path).expect("ar archive should be readable");
|
|
1646
|
+
assert_eq!(&bytes[..8], b"!<arch>\n");
|
|
1647
|
+
|
|
1648
|
+
let mut members = std::collections::BTreeMap::new();
|
|
1649
|
+
let mut offset = 8;
|
|
1650
|
+
while offset < bytes.len() {
|
|
1651
|
+
let header = &bytes[offset..offset + 60];
|
|
1652
|
+
let name = std::str::from_utf8(&header[0..16])
|
|
1653
|
+
.expect("member name should be utf-8")
|
|
1654
|
+
.trim()
|
|
1655
|
+
.trim_end_matches('/')
|
|
1656
|
+
.to_string();
|
|
1657
|
+
let size = std::str::from_utf8(&header[48..58])
|
|
1658
|
+
.expect("member size should be utf-8")
|
|
1659
|
+
.trim()
|
|
1660
|
+
.parse::<usize>()
|
|
1661
|
+
.expect("member size should parse");
|
|
1662
|
+
let data_start = offset + 60;
|
|
1663
|
+
let data_end = data_start + size;
|
|
1664
|
+
members.insert(name, bytes[data_start..data_end].to_vec());
|
|
1665
|
+
offset = data_end + (size % 2);
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
members
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
fn read_tar_file(archive: &[u8], path: &str) -> String {
|
|
1672
|
+
let decoder = flate2::read::GzDecoder::new(archive);
|
|
1673
|
+
let mut archive = tar::Archive::new(decoder);
|
|
1674
|
+
for entry in archive.entries().expect("tar entries should read") {
|
|
1675
|
+
let mut entry = entry.expect("tar entry should read");
|
|
1676
|
+
let entry_path = entry
|
|
1677
|
+
.path()
|
|
1678
|
+
.expect("tar path should read")
|
|
1679
|
+
.to_string_lossy()
|
|
1680
|
+
.trim_start_matches("./")
|
|
1681
|
+
.to_string();
|
|
1682
|
+
if entry_path == path {
|
|
1683
|
+
let mut contents = String::new();
|
|
1684
|
+
entry
|
|
1685
|
+
.read_to_string(&mut contents)
|
|
1686
|
+
.expect("tar file should read");
|
|
1687
|
+
return contents;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
panic!("tar file was not found: {path}");
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
fn tar_contains(archive: &[u8], path: &str) -> bool {
|
|
1695
|
+
let decoder = flate2::read::GzDecoder::new(archive);
|
|
1696
|
+
let mut archive = tar::Archive::new(decoder);
|
|
1697
|
+
archive
|
|
1698
|
+
.entries()
|
|
1699
|
+
.expect("tar entries should read")
|
|
1700
|
+
.any(|entry| {
|
|
1701
|
+
entry
|
|
1702
|
+
.expect("tar entry should read")
|
|
1703
|
+
.path()
|
|
1704
|
+
.expect("tar path should read")
|
|
1705
|
+
.to_string_lossy()
|
|
1706
|
+
.trim_start_matches("./")
|
|
1707
|
+
== path
|
|
1708
|
+
})
|
|
1709
|
+
}
|
|
1710
|
+
}
|