electron-cli 0.3.0-alpha.3 → 0.3.0-alpha.5

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.
@@ -0,0 +1,432 @@
1
+ use std::{
2
+ fs,
3
+ path::{Path, PathBuf},
4
+ time::{SystemTime, UNIX_EPOCH},
5
+ };
6
+
7
+ use anyhow::{bail, Context, Result};
8
+ use camino::Utf8PathBuf;
9
+ use serde::Serialize;
10
+
11
+ use crate::{
12
+ cli::{MakeArgs, PublishArgs},
13
+ commands::make::{self, MakeReport},
14
+ output,
15
+ };
16
+
17
+ #[derive(Debug, Serialize)]
18
+ struct PublishReport {
19
+ make: MakeReport,
20
+ publisher: String,
21
+ channel: String,
22
+ destination_dir: Utf8PathBuf,
23
+ destination_artifact: Utf8PathBuf,
24
+ manifest: Utf8PathBuf,
25
+ skip_make: bool,
26
+ dry_run: bool,
27
+ status: PublishStatus,
28
+ published_at_unix_seconds: Option<u64>,
29
+ warnings: Vec<String>,
30
+ }
31
+
32
+ #[derive(Debug, Serialize)]
33
+ #[serde(rename_all = "kebab-case")]
34
+ enum PublishStatus {
35
+ Planned,
36
+ Published,
37
+ }
38
+
39
+ #[derive(Debug, Serialize)]
40
+ struct PublishManifest {
41
+ schema_version: u8,
42
+ publisher: String,
43
+ channel: String,
44
+ app_name: String,
45
+ package_name: Option<String>,
46
+ package_version: Option<String>,
47
+ platform: String,
48
+ arch: String,
49
+ target: String,
50
+ published_at_unix_seconds: u64,
51
+ artifacts: Vec<PublishedArtifact>,
52
+ }
53
+
54
+ #[derive(Debug, Serialize)]
55
+ struct PublishedArtifact {
56
+ file: String,
57
+ path: Utf8PathBuf,
58
+ size: u64,
59
+ }
60
+
61
+ pub fn run(args: PublishArgs) -> Result<()> {
62
+ let mut report = build_report(&args)?;
63
+
64
+ if args.dry_run {
65
+ return print_report(&report, args.json);
66
+ }
67
+
68
+ execute_publish(&mut report, &args)?;
69
+ report.status = PublishStatus::Published;
70
+
71
+ print_report(&report, args.json)
72
+ }
73
+
74
+ fn build_report(args: &PublishArgs) -> Result<PublishReport> {
75
+ let make_args = MakeArgs {
76
+ cwd: args.cwd.clone(),
77
+ out_dir: args.out_dir.clone(),
78
+ name: args.name.clone(),
79
+ platform: args.platform.clone(),
80
+ arch: args.arch.clone(),
81
+ target: args.target,
82
+ skip_package: false,
83
+ force: args.force,
84
+ dry_run: false,
85
+ json: false,
86
+ };
87
+ let make = make::build_report(&make_args)?;
88
+ let root = Path::new(make.package().project().root.as_str());
89
+ let publish_root = resolve_destination(root, &args.to);
90
+ let destination_dir = publish_root
91
+ .join(&args.channel)
92
+ .join(make.package().platform())
93
+ .join(make.package().arch());
94
+ let artifact_name = make
95
+ .artifact()
96
+ .file_name()
97
+ .context("Make artifact path has no file name")?;
98
+ let destination_artifact = destination_dir.join(artifact_name);
99
+ let manifest = destination_dir.join("manifest.json");
100
+
101
+ let mut warnings = make.warnings().to_vec();
102
+ if args.skip_make && !Path::new(make.artifact().as_str()).exists() {
103
+ warnings.push(format!(
104
+ "Make artifact does not exist: {}.",
105
+ make.artifact()
106
+ ));
107
+ }
108
+ if destination_artifact.exists() && !args.force {
109
+ warnings.push(format!(
110
+ "Publish artifact already exists: {}. Use --force to overwrite it.",
111
+ destination_artifact.display()
112
+ ));
113
+ }
114
+ if manifest.exists() && !args.force {
115
+ warnings.push(format!(
116
+ "Publish manifest already exists: {}. Use --force to overwrite it.",
117
+ manifest.display()
118
+ ));
119
+ }
120
+
121
+ Ok(PublishReport {
122
+ make,
123
+ publisher: args.publisher.as_str().to_string(),
124
+ channel: args.channel.clone(),
125
+ destination_dir: utf8_path(destination_dir)?,
126
+ destination_artifact: utf8_path(destination_artifact)?,
127
+ manifest: utf8_path(manifest)?,
128
+ skip_make: args.skip_make,
129
+ dry_run: args.dry_run,
130
+ status: PublishStatus::Planned,
131
+ published_at_unix_seconds: None,
132
+ warnings,
133
+ })
134
+ }
135
+
136
+ fn execute_publish(report: &mut PublishReport, args: &PublishArgs) -> Result<()> {
137
+ if !args.skip_make {
138
+ let make_args = MakeArgs {
139
+ cwd: args.cwd.clone(),
140
+ out_dir: args.out_dir.clone(),
141
+ name: args.name.clone(),
142
+ platform: args.platform.clone(),
143
+ arch: args.arch.clone(),
144
+ target: args.target,
145
+ skip_package: false,
146
+ force: args.force,
147
+ dry_run: false,
148
+ json: false,
149
+ };
150
+ make::execute_make(&mut report.make, &make_args)?;
151
+ report.make.mark_made()?;
152
+ } else if !Path::new(report.make.artifact().as_str()).exists() {
153
+ bail!(
154
+ "Make artifact does not exist: {}. Run without --skip-make or run electron-cli make first.",
155
+ report.make.artifact()
156
+ );
157
+ }
158
+
159
+ let destination_artifact = Path::new(report.destination_artifact.as_str());
160
+ let manifest = Path::new(report.manifest.as_str());
161
+
162
+ for path in [destination_artifact, manifest] {
163
+ if path.exists() {
164
+ if args.force {
165
+ fs::remove_file(path)
166
+ .with_context(|| format!("Could not remove {}", path.display()))?;
167
+ } else {
168
+ bail!(
169
+ "Publish output already exists: {}. Use --force to overwrite it.",
170
+ path.display()
171
+ );
172
+ }
173
+ }
174
+ }
175
+
176
+ fs::create_dir_all(report.destination_dir.as_str())
177
+ .with_context(|| format!("Could not create {}", report.destination_dir))?;
178
+ fs::copy(report.make.artifact().as_str(), destination_artifact).with_context(|| {
179
+ format!(
180
+ "Could not publish {} to {}",
181
+ report.make.artifact(),
182
+ destination_artifact.display()
183
+ )
184
+ })?;
185
+
186
+ let published_at_unix_seconds = now_unix_seconds()?;
187
+ report.published_at_unix_seconds = Some(published_at_unix_seconds);
188
+ let manifest_json =
189
+ serde_json::to_string_pretty(&build_manifest(report, published_at_unix_seconds)?)?;
190
+ fs::write(manifest, format!("{manifest_json}\n"))
191
+ .with_context(|| format!("Could not write {}", manifest.display()))?;
192
+
193
+ Ok(())
194
+ }
195
+
196
+ fn build_manifest(
197
+ report: &PublishReport,
198
+ published_at_unix_seconds: u64,
199
+ ) -> Result<PublishManifest> {
200
+ let destination_artifact = Path::new(report.destination_artifact.as_str());
201
+ let artifact_size = fs::metadata(destination_artifact)
202
+ .with_context(|| format!("Could not stat {}", destination_artifact.display()))?
203
+ .len();
204
+ let artifact_file = destination_artifact
205
+ .file_name()
206
+ .and_then(|name| name.to_str())
207
+ .context("Published artifact path has no UTF-8 file name")?
208
+ .to_string();
209
+
210
+ Ok(PublishManifest {
211
+ schema_version: 1,
212
+ publisher: report.publisher.clone(),
213
+ channel: report.channel.clone(),
214
+ app_name: report.make.package().app_name().to_string(),
215
+ package_name: report.make.package().project().name.clone(),
216
+ package_version: report.make.package().project().version.clone(),
217
+ platform: report.make.package().platform().to_string(),
218
+ arch: report.make.package().arch().to_string(),
219
+ target: report.make.target().to_string(),
220
+ published_at_unix_seconds,
221
+ artifacts: vec![PublishedArtifact {
222
+ file: artifact_file,
223
+ path: report.destination_artifact.clone(),
224
+ size: artifact_size,
225
+ }],
226
+ })
227
+ }
228
+
229
+ fn print_report(report: &PublishReport, json: bool) -> Result<()> {
230
+ if json {
231
+ return output::json(report);
232
+ }
233
+
234
+ println!("electron-cli publish");
235
+ println!();
236
+ println!("Project");
237
+ println!(" root: {}", report.make.package().project().root);
238
+ match report.make.package().project().package_label() {
239
+ Some(label) => println!(" package: {label}"),
240
+ None => println!(" package: not found"),
241
+ }
242
+ println!(" app name: {}", report.make.package().app_name());
243
+ println!(
244
+ " target: {} {} {}",
245
+ report.make.target(),
246
+ report.make.package().platform(),
247
+ report.make.package().arch()
248
+ );
249
+ println!(" publisher: {}", report.publisher);
250
+ println!(" channel: {}", report.channel);
251
+ println!(" status: {}", report.status.as_str());
252
+
253
+ println!();
254
+ println!("Publish");
255
+ println!(" artifact: {}", report.destination_artifact);
256
+ println!(" manifest: {}", report.manifest);
257
+
258
+ if !report.warnings.is_empty() {
259
+ println!();
260
+ println!("Warnings");
261
+ for warning in &report.warnings {
262
+ println!(" {warning}");
263
+ }
264
+ }
265
+
266
+ Ok(())
267
+ }
268
+
269
+ fn resolve_destination(root: &Path, destination: &Path) -> PathBuf {
270
+ if destination.is_absolute() {
271
+ destination.to_path_buf()
272
+ } else {
273
+ root.join(destination)
274
+ }
275
+ }
276
+
277
+ fn now_unix_seconds() -> Result<u64> {
278
+ Ok(SystemTime::now()
279
+ .duration_since(UNIX_EPOCH)
280
+ .context("System clock is before the Unix epoch")?
281
+ .as_secs())
282
+ }
283
+
284
+ fn utf8_path(path: PathBuf) -> Result<Utf8PathBuf> {
285
+ Utf8PathBuf::from_path_buf(path).map_err(|path| {
286
+ anyhow::anyhow!(
287
+ "Path contains invalid UTF-8 and cannot be represented in JSON: {}",
288
+ path.display()
289
+ )
290
+ })
291
+ }
292
+
293
+ impl PublishStatus {
294
+ fn as_str(&self) -> &'static str {
295
+ match self {
296
+ PublishStatus::Planned => "planned",
297
+ PublishStatus::Published => "published",
298
+ }
299
+ }
300
+ }
301
+
302
+ #[cfg(test)]
303
+ mod tests {
304
+ use super::*;
305
+
306
+ #[test]
307
+ fn builds_local_publish_report() {
308
+ let root = unique_temp_dir("plan");
309
+ write_package_json(&root);
310
+ write_app_file(&root);
311
+ write_fake_electron_dist(&root);
312
+
313
+ let args = publish_args(root.clone(), true);
314
+ let report = build_report(&args).expect("report should build");
315
+
316
+ assert_eq!(report.publisher, "local");
317
+ assert_eq!(report.channel, "default");
318
+ assert!(Path::new(report.destination_artifact.as_str()).ends_with(
319
+ PathBuf::from("out")
320
+ .join("publish")
321
+ .join("local")
322
+ .join("default")
323
+ .join(report.make.package().platform())
324
+ .join(report.make.package().arch())
325
+ .join(format!(
326
+ "starter-app-{}-{}.zip",
327
+ report.make.package().platform(),
328
+ report.make.package().arch()
329
+ ))
330
+ ));
331
+
332
+ let _ = fs::remove_dir_all(root);
333
+ }
334
+
335
+ #[test]
336
+ fn publishes_make_artifact_to_local_directory() {
337
+ let root = unique_temp_dir("execute");
338
+ write_package_json(&root);
339
+ write_app_file(&root);
340
+ write_fake_electron_dist(&root);
341
+
342
+ let args = publish_args(root.clone(), false);
343
+ let mut report = build_report(&args).expect("report should build");
344
+
345
+ execute_publish(&mut report, &args).expect("publish should succeed");
346
+
347
+ assert!(Path::new(report.destination_artifact.as_str()).exists());
348
+ assert!(Path::new(report.manifest.as_str()).exists());
349
+ let manifest =
350
+ fs::read_to_string(report.manifest.as_str()).expect("manifest should be readable");
351
+ assert!(manifest.contains("\"publisher\": \"local\""));
352
+ assert!(manifest.contains("\"app_name\": \"starter-app\""));
353
+
354
+ let _ = fs::remove_dir_all(root);
355
+ }
356
+
357
+ #[test]
358
+ fn skip_make_requires_existing_artifact() {
359
+ let root = unique_temp_dir("skip-make");
360
+ write_package_json(&root);
361
+ write_app_file(&root);
362
+ write_fake_electron_dist(&root);
363
+
364
+ let mut args = publish_args(root.clone(), false);
365
+ args.skip_make = true;
366
+ let mut report = build_report(&args).expect("report should build");
367
+
368
+ assert!(execute_publish(&mut report, &args).is_err());
369
+
370
+ let _ = fs::remove_dir_all(root);
371
+ }
372
+
373
+ fn publish_args(root: PathBuf, dry_run: bool) -> PublishArgs {
374
+ PublishArgs {
375
+ cwd: root,
376
+ out_dir: PathBuf::from("out"),
377
+ name: None,
378
+ platform: None,
379
+ arch: None,
380
+ target: crate::cli::MakeTarget::Zip,
381
+ publisher: crate::cli::PublishTarget::Local,
382
+ to: PathBuf::from("out/publish/local"),
383
+ channel: "default".to_string(),
384
+ skip_make: false,
385
+ force: false,
386
+ dry_run,
387
+ json: true,
388
+ }
389
+ }
390
+
391
+ fn write_package_json(root: &Path) {
392
+ fs::write(
393
+ root.join("package.json"),
394
+ r#"{"name":"starter-app","version":"0.1.0","main":"src/main.js","devDependencies":{"electron":"30.0.0"}}"#,
395
+ )
396
+ .expect("package.json should be written");
397
+ }
398
+
399
+ fn write_app_file(root: &Path) {
400
+ fs::create_dir_all(root.join("src")).expect("src should be created");
401
+ fs::write(root.join("src/main.js"), "console.log('hello');")
402
+ .expect("main file should be written");
403
+ }
404
+
405
+ fn write_fake_electron_dist(root: &Path) {
406
+ let dist = root.join("node_modules/electron/dist");
407
+ if cfg!(target_os = "macos") {
408
+ let app = dist.join("Electron.app/Contents/MacOS");
409
+ fs::create_dir_all(&app).expect("fake macOS electron app should be created");
410
+ fs::write(app.join("Electron"), "").expect("fake macOS binary should be written");
411
+ } else if cfg!(target_os = "windows") {
412
+ fs::create_dir_all(&dist).expect("fake electron dist should be created");
413
+ fs::write(dist.join("electron.exe"), "").expect("fake exe should be written");
414
+ } else {
415
+ fs::create_dir_all(&dist).expect("fake electron dist should be created");
416
+ fs::write(dist.join("electron"), "").expect("fake binary should be written");
417
+ }
418
+ }
419
+
420
+ fn unique_temp_dir(label: &str) -> PathBuf {
421
+ let nanos = std::time::SystemTime::now()
422
+ .duration_since(std::time::UNIX_EPOCH)
423
+ .expect("clock should be after epoch")
424
+ .as_nanos();
425
+ let path = std::env::temp_dir().join(format!(
426
+ "electron-cli-publish-{label}-{}-{nanos}",
427
+ std::process::id()
428
+ ));
429
+ fs::create_dir_all(&path).expect("temp dir should be created");
430
+ path
431
+ }
432
+ }
package/src/main.rs CHANGED
@@ -21,8 +21,10 @@ fn run() -> Result<()> {
21
21
  Commands::Doctor(args) => commands::doctor::run(args),
22
22
  Commands::Init(args) => commands::init::run(args),
23
23
  Commands::Inspect(args) => commands::inspect::run(args),
24
+ Commands::Make(args) => commands::make::run(args),
24
25
  Commands::Package(args) => commands::package::run(args),
25
26
  Commands::Plan(args) => commands::plan::run(args),
27
+ Commands::Publish(args) => commands::publish::run(args),
26
28
  Commands::Start(args) => commands::start::run(args),
27
29
  }
28
30
  }