machine-check-gui 0.6.0

Utility crate for the formal verification tool machine-check
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
use std::{
    collections::BTreeMap,
    ffi::OsString,
    fs,
    path::{Path, PathBuf},
    process::Command,
};

use anyhow::anyhow;
use tempfile::TempDir;
use tinyjson::JsonValue;
use toml_edit::Table;

/// A warning macro that uses Cargo warning, since stdout of build.rs is processed by Cargo.
///
/// The newlines must be escaped as Cargo will take a newline to signify end of the command.
macro_rules! warn {
    ($($tokens: tt)*) => {
        println!("cargo:warning={}", format!($($tokens)*).replace('\n', "\\n"))
    }
}

/// A debug macro that uses Cargo warning only if the corresponding feature is enabled.
macro_rules! debug {
    ($($tokens: tt)*) => {
        #[cfg(feature = "build-log-debug")]
        { println!("cargo:warning=debug: {}", format!($($tokens)*).replace('\n', "\\n")); }
        #[cfg(not(feature = "build-log-debug"))]
        { let _ = format!($($tokens)*);}
    }
}

/// Whether to build the WebAssembly frontend.
///
/// This can be further overriden by ENV_FORCE_NO_BUILD.
const ENV_BUILD_FRONTEND: &str = "MACHINE_CHECK_GUI_BUILD_FRONTEND";

/// Whether to build the WebAssembly frontend.
///
/// Overrides ENV_BUILD_FRONTEND if present and true.
const ENV_FORCE_NO_BUILD: &str = "MACHINE_CHECK_GUI_FORCE_NO_BUILD";

/// Whether to prepare the WebAssembly frontend artefacts for distribution.
///
///
const ENV_PREPARE_FRONTEND: &str = "MACHINE_CHECK_GUI_PREPARE_FRONTEND";

/// Name of the environment variable that determines where the frontend will be built.
///
/// If not defined, a temporary directory will be used.
///
/// It is useful to set this variable when working on the machine-check-gui itself.
/// allowing compilation speedup due to caching. The path must not be a member
/// of a higher directory level workspace, since the frontend package will be its
/// own workspace and Cargo gives an error if there is a workspace within a workspace.
///
/// In the standard case when we are not working on machine-check-gui, the environment variable
/// will be unset and we will run this build.rs (and frontend build) only once after each cleanup,
/// so the compilation speedup is not important. We will create a temporary directory for
/// the frontend build, and delete it afterwards.
///
/// This is not used if preparing.
const ENV_WASM_DIR: &str = "MACHINE_CHECK_GUI_WASM_DIR";

/// Name of the environment variable that determines whether we should postpone build errors.
///
/// If not defined, build errors will not be postponed. If defined, must be 'true' or 'false'
/// with the letter case ignored, otherwise, build.rs will panic.
///
/// Postponement should be usually disabled, but in case we are working on machine-check-gui itself,
/// we want build.rs to succeed so that machine-check-gui will be compiled traditionally afterwards,
/// prompting most errors to correctly show up in rust-analyzer.
/// We make sure that the frontend build produced the frontend WASM in lib.rs by including,
/// so an error should always be produced if the frontend build fails.
const ENV_POSTPONE_ERRORS: &str = "MACHINE_CHECK_GUI_POSTPONE_ERRORS";

const ENV_VARIABLES: [&str; 2] = [ENV_WASM_DIR, ENV_POSTPONE_ERRORS];

/// The main function. Simply calls the run function and and panics on error.
fn main() {
    if let Err(err) = run() {
        panic!("Error building WASM frontend: {}", err);
    }
}

/// The run function.
///
/// Either builds the fronend or, by default, arranges the sources and checks if they match
/// the prepared WebAssembly files. This alleviates the need for every user to install
/// the appropriate WebAssembly build tools.
fn run() -> anyhow::Result<()> {
    let prepare = bool_env(ENV_PREPARE_FRONTEND, false)?;
    let mut should_build = bool_env(ENV_BUILD_FRONTEND, false)?;
    if bool_env(ENV_FORCE_NO_BUILD, false)? {
        warn!("Forcing no frontend WASM build.");
        should_build = false;
    }

    let arrangement = arrange(should_build, prepare)?;
    if should_build {
        build(&arrangement)?;
    } else {
        ensure_wasm_hash(&arrangement.artifact_dir, arrangement.hex_hash.clone())?;
    }

    // Close and remove the temporary directory if we created it.
    if let Some(frontend_package_tempdir) = arrangement.frontend_package_tempdir {
        if let Err(err) = frontend_package_tempdir.close() {
            // just warn if we did not succeed
            warn!("Could not close WASM temporary directory: {}", err)
        }
    };
    Ok(())
}

fn ensure_wasm_hash(artifact_dir: &Path, hex_hash: String) -> anyhow::Result<()> {
    let prebuilt_hex_hash =
        fs::read_to_string(artifact_dir.join(HASH_FILE_NAME)).map_err(|err| {
            anyhow!(
                "Could not read the hash file of prebuilt WebAssembly frontend: {}",
                err
            )
        })?;
    if hex_hash != prebuilt_hex_hash {
        return Err(anyhow!(
            "The prebuilt WebAssembly frontend does not match the source code (our hash {}, prebuilt hash {})",hex_hash, prebuilt_hex_hash
        ));
    }
    Ok(())
}

/// Build the WebAssembly frontend.
///
/// It does not seem to be currently possible to build cross-target directly in one cargo build call.
/// We also cannot call cargo build on the current package using a different target as this directory
/// is already locked by cargo (and we would wait for the lock forever).
///
/// So we need to be clever, arrange and build the frontend somewhere else. This function gets the
/// arrangement data and and builds.
fn build(arrangement: &Arrangement) -> anyhow::Result<()> {
    // Remove the artefact directory.
    let _ = fs::remove_dir_all(&arrangement.artifact_dir);

    // Compile the frontend package, only warning if we should postpone errors.
    match compile_frontend_package(arrangement) {
        Ok(None) => {}
        Ok(Some(postponed)) => {
            warn!(
                "WASM frontend build failed (postponing error): {}",
                postponed
            )
        }
        Err(err) => return Err(anyhow!("Build failed: {}", err)),
    }

    Ok(())
}

struct Arrangement {
    prepare: bool,
    this_package_dir: PathBuf,
    frontend_package_tempdir: Option<TempDir>,
    frontend_package_dir: PathBuf,
    artifact_dir: PathBuf,
    postpone_build_errors: bool,
    hex_hash: String,
}

fn arrange(build: bool, prepare: bool) -> anyhow::Result<Arrangement> {
    // Change the current working directory to the cargo manifest directory of our package (not the workspace).
    let this_package_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
    std::env::set_current_dir(this_package_dir)
        .expect("Should be able to move to manifest directory");

    // Only rerun build.rs if frontend-related files, directories, or environment variables change.
    COPY_DIRECTORIES
        .into_iter()
        .chain(COPY_FILES)
        .chain(SPECIAL_FILES)
        .for_each(|path| {
            println!("cargo::rerun-if-changed={}", path);
        });
    for env_variable in ENV_VARIABLES {
        println!("cargo::rerun-if-env-changed={}", env_variable);
    }

    // Determine if we should postpone build errors for the frontend.
    //
    // In the standard case when we are not working on machine-check-gui, no errors should be present
    // at all, so we can just safely not postpone build errors.

    let postpone_build_errors = bool_env(ENV_POSTPONE_ERRORS, false)?;

    let mut wasm_dir = std::env::var_os(ENV_WASM_DIR);

    // Get the location where frontend WASM package should be placed.
    let (frontend_package_dir, frontend_package_tempdir) = if build {
        if prepare {
            // use a temporary folder if preparing
            wasm_dir = None;
        }
        match wasm_dir {
            Some(wasm_dir) => {
                // canonicalize the WASM directory using the current working directory, which is the cargo manifest directory
                fs::create_dir_all(wasm_dir.clone())
                    .expect("Should be able to move to WASM directory");
                let wasm_dir = fs::canonicalize(wasm_dir)
                    .expect("Should be able to canonicalize WASM directory");

                (wasm_dir, None)
            }
            None => {
                // Create a temporary directory.
                let tempdir = tempfile::TempDir::with_prefix("wasm_crate_")
                    .expect("Should be able to create a temporary directory");

                (tempdir.path().to_path_buf(), Some(tempdir))
            }
        }
    } else {
        // If not building, we can just put this in OUT_DIR as it will not be used except for computing the hash.
        let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR should be set"));
        (out_dir.join("machine-check-gui-wasm"), None)
    };

    debug!("Frontend package directory: {:?}", frontend_package_dir);

    // Get the package and workspace Cargo.toml path. This works as we are still in the cargo manifest directory.
    let package_toml_path = cargo_toml_path(false)?;
    let workspace_toml_path = cargo_toml_path(true)?;

    // If the package and workspace Cargo.toml path is the same, the package is the root of the workspace.
    let mut workspace_toml_path = if package_toml_path != workspace_toml_path {
        Some(workspace_toml_path)
    } else {
        None
    };

    // If we are preparing for deployment, ignore workspace Cargo.toml.
    if prepare {
        warn!("Preparing the frontend WebAssembly for deployment. Make sure you know what you are doing.");
        // If we are preparing, delete the previous frontend package first so that it is completely fresh.
        let _ = fs::remove_dir_all(&frontend_package_dir);
        if workspace_toml_path.is_some() {
            warn!("Overriding workspace build due to deployment preparation.");
            workspace_toml_path = None;
        }
    }

    let artifact_dir = this_package_dir.join(ARTIFACT_DIR);

    // Arrange the frontend package.
    let hex_hash = arrange_frontend_package(
        this_package_dir,
        &frontend_package_dir,
        package_toml_path,
        workspace_toml_path,
    )
    .map_err(|err| anyhow!("Package preparation failed: {}", err))?;

    Ok(Arrangement {
        prepare,
        this_package_dir: this_package_dir.to_path_buf(),
        frontend_package_tempdir,
        frontend_package_dir: frontend_package_dir.to_path_buf(),
        artifact_dir: artifact_dir.to_path_buf(),
        postpone_build_errors,
        hex_hash,
    })
}

const COPY_DIRECTORIES: [&str; 2] = ["src/shared", "src/frontend"];
const COPY_FILES: [&str; 2] = ["src/frontend.rs", "src/shared.rs"];
const LIB_RS: &str = "src/lib.rs";
const CARGO_TOML: &str = "Cargo.toml";
const RUST_TOOLCHAIN_TOML: &str = "rust-toolchain.toml";
const SPECIAL_FILES: [&str; 2] = [LIB_RS, CARGO_TOML];

const ARTIFACT_DIR: &str = "content/wasm";

const HASH_FILE_NAME: &str = "hash.hex";

/// Arrange the frontend package directory for building.
///
/// After cleaning the frontend package directory, we will copy the frontend sources to the frontend
/// package directory appropriately, and add a custom lib.rs to ensure only the frontend is compiled.
///
/// For Cargo.toml, we will take our own machine-check-gui Cargo.toml as a starting point.
/// Since the frontend package directory may or may not be the member of the same (or even another)
/// workspace, we will add a [workspace] entry to force Cargo to error if the frontend package directory
/// is a member of another workspace. To ensure we still have [patch] and [profile] from the workspace
/// machine-check-gui is in, we will process its Cargo.toml (if it exists) and move them. We will panic
/// on the deprecated [replace] in workspace, and also move [workspace] dependencies and lints.
///
/// While doing this, we must convert the local paths in machine-check-gui [dependencies] and workspace
/// [patch] and [workspace.dependencies], ensuring they are absolute. We will simply do this by taking
/// the canonical path while in the appropriate working directory.
///
/// This should handle a reasonable amount of Cargo.toml configuration, especially when building
/// standalone and when building the official machine-check repository.
///
/// This function can change the working directory.
fn arrange_frontend_package(
    this_package_dir: &Path,
    frontend_package_dir: &Path,
    package_toml_path: PathBuf,
    workspace_toml_path: Option<PathBuf>,
) -> anyhow::Result<String> {
    // There should be no rust-toolchain.toml in our directory.
    // It is OK if it is in a containing workspace.
    if this_package_dir.join(RUST_TOOLCHAIN_TOML).exists() {
        return Err(anyhow!(
            "A rust-toolchain.toml in the package directory is not supported"
        ));
    }

    // Copy the appropriate directories and files to the WASM package directory.
    for copy_directory in COPY_DIRECTORIES {
        copy_dir_all(
            this_package_dir.join(copy_directory),
            frontend_package_dir.join(copy_directory),
        )?;
    }

    for copy_file in COPY_FILES {
        fs::copy(
            this_package_dir.join(copy_file),
            frontend_package_dir.join(copy_file),
        )?;
    }

    // Handle lib.rs specially: just declare the shared and frontend.
    fs::write(
        frontend_package_dir.join(LIB_RS),
        "#![allow(clippy::all)]\npub mod shared; mod frontend;\n",
    )?;

    // Handle the package Cargo.toml specially.
    let cargo_toml = fs::read_to_string(this_package_dir.join(CARGO_TOML))?;
    let mut cargo_toml: toml_edit::DocumentMut = cargo_toml.parse()?;

    // If there is [workspace], [patch], or [replace] in the package Cargo.toml, return an error,
    // so that we do not have to do complicated patching.
    if cargo_toml.contains_key("workspace") {
        return Err(anyhow!(
            "Workspace entry in package Cargo.toml not supported"
        ));
    }
    if cargo_toml.contains_key("patch") {
        return Err(anyhow!("Patch entry in package Cargo.toml not supported"));
    }
    if cargo_toml.contains_key("replace") {
        return Err(anyhow!("Replace entry in package Cargo.toml not supported"));
    }

    // Update the package name to prevent potential confusion.
    // Insert the workspace table so it is its own workspace.
    // Also set publish=false as it is just a generated package.

    cargo_toml["package"]["name"] = "machine-check-gui-wasm".into();
    cargo_toml.insert("workspace", toml_edit::Item::Table(Table::new()));
    cargo_toml["package"]["publish"] = false.into();

    // Next, canonicalize the paths in the package Cargo.toml.
    // For simplicity, only process [dependencies].
    canonicalize_paths(&package_toml_path, &mut cargo_toml["dependencies"])?;

    let mut patched_repository_package_paths: BTreeMap<String, BTreeMap<String, String>> =
        BTreeMap::new();

    // Adjust using the workspace Cargo.toml if there is one.
    if let Some(workspace_toml_path) = workspace_toml_path {
        // Read and parse the workspace.
        let workspace_toml = fs::read_to_string(&workspace_toml_path)?;
        let workspace_toml: toml_edit::DocumentMut = workspace_toml.parse()?;

        let package_workspace = cargo_toml
            .get_mut("workspace")
            .unwrap()
            .as_table_mut()
            .unwrap();

        // Apply [workspace.dependencies] with adjusted paths if it exists.
        if let Some(workspace_dependencies) = workspace_toml["workspace"].get("dependencies") {
            let mut workspace_dependencies = workspace_dependencies.clone();
            canonicalize_paths(&workspace_toml_path, &mut workspace_dependencies)?;
            package_workspace.insert("dependencies", workspace_dependencies);
        }

        // Just copy over the [workspace.lints] if they exist.
        if let Some(workspace_lints) = workspace_toml["workspace"].get("lints") {
            package_workspace.insert("lints", workspace_lints.clone());
        }

        // Return an error if there is a [replace] section, it is deprecated in favour of [patch] anyway.

        if workspace_toml.contains_key("replace") {
            return Err(anyhow!(
                "Replace entry in workspace Cargo.toml not supported. Consider using patch."
            ));
        }

        // Apply [patch] with adjusted paths if it exists.
        if let Some(patch) = workspace_toml.get("patch") {
            let mut patch = patch.clone();
            // Patches are a list for each repository, so process everything in a loop.
            let Some(patch_table) = patch.as_table_mut() else {
                return Err(anyhow!(
                    "Unexpected non-table workspace patch in {:?}",
                    workspace_toml_path
                ));
            };

            for (repository_key, repository_value) in patch_table.iter_mut() {
                let patched_package_paths =
                    canonicalize_paths(&workspace_toml_path, repository_value)?;
                let entry = patched_repository_package_paths
                    .entry(repository_key.to_string())
                    .or_default();

                entry.extend(patched_package_paths.into_iter());
            }
            debug!("Applying workspace patch to WASM package");
            cargo_toml.insert("patch", patch);
        }

        // Copy workspace rust-toolchain.toml if it exists.
        let workspace_rust_toolchain_toml = workspace_toml_path
            .parent()
            .ok_or(anyhow!("Workspace Cargo.toml should have a parent"))?
            .join(RUST_TOOLCHAIN_TOML);
        if workspace_rust_toolchain_toml.exists() {
            fs::copy(
                workspace_rust_toolchain_toml.clone(),
                frontend_package_dir.join(RUST_TOOLCHAIN_TOML),
            )
            .map_err(|err| anyhow!("Workspace rust-toolchain.toml could not be copied: {}", err))?;
        }
        // rerun the build script if the workspace Cargo.toml or rust-toolchain.toml changes
        cargo_rerun_if_path_changed(&workspace_toml_path)?;
        cargo_rerun_if_path_changed(&workspace_rust_toolchain_toml)?;
    }

    // Ensure build.rs is rebuilt when something is changed in patched dependency paths.

    let dependencies = cargo_toml
        .get("dependencies")
        .ok_or(anyhow!("Expected dependencies in Cargo.toml"))?;
    let dependencies = dependencies
        .as_table()
        .ok_or(anyhow!("Unexpected non-table dependencies in Cargo.toml"))?;

    for (dependency_name, dependency) in dependencies {
        let registry = if let Some(dependency) = dependency.as_table_like() {
            if let Some(registry) = dependency.get("registry") {
                Some(
                    registry
                        .as_str()
                        .ok_or(anyhow!("Unexpected non-string registry in Cargo.toml"))?,
                )
            } else {
                None
            }
        } else {
            None
        };
        let registry = registry.unwrap_or("crates-io");

        if let Some(patched_package_paths) = patched_repository_package_paths.get(registry) {
            if let Some(patched_path) = patched_package_paths.get(dependency_name) {
                debug!(
                    "Ensuring rebuild on repository {} dependency {} patched path {:?}",
                    dependency_name, registry, patched_path
                );
                // rerun the build script if the dependency changes
                println!("cargo::rerun-if-changed={}", patched_path);
            }
        }
    }

    // As cargo changes the original Cargo.toml when it is being published,
    // compute the frontend package directory hash before Cargo.toml is added.
    let hex_hash = directory_hex_hash(String::from(
        frontend_package_dir
            .to_str()
            .expect("Frontend package directory path should be UTF-8"),
    ))?;

    // We have done the important adjustments, write the Cargo.toml to the frontend package directory.
    fs::write(
        frontend_package_dir.join(CARGO_TOML),
        cargo_toml.to_string(),
    )?;

    // Add a wildcard .gitignore to the created WASM package as it is generated.
    fs::write(frontend_package_dir.join(".gitignore"), "*\n")?;

    Ok(hex_hash)
}

/// Compile the frontend and process it with wasm-bindgen, producing the final artefacts.
///
/// After arranging the frontend package, we will cargo build it with the WASM target, and run
/// wasm-bindgen on the built artefacts afterwards, producing the WASM files into the directory
/// from which they will be served. Before the build, we will empty the directory so that we never get
/// stale artefacts. The artefact existence is checked in machine-check-gui lib.rs, producing an error
/// if they do not exist after this build.rs executes normally.
fn compile_frontend_package(arrangement: &Arrangement) -> anyhow::Result<Option<anyhow::Error>> {
    // Prepare arguments for cargo build.
    // We want the target-dir (where to build to) to be the frontend package target subdirectory.
    let cargo_target_dir: PathBuf = arrangement.frontend_package_dir.join("target");
    let cargo_target_dir_arg = create_equals_arg("target-dir", &cargo_target_dir);

    // Prepare cargo build for the WASM target.
    std::env::set_current_dir(&arrangement.frontend_package_dir)
        .map_err(|err| anyhow!("Cannot set current frontend package dir: {}", err))?;
    let mut cargo_build = Command::new("cargo");
    cargo_build
        .current_dir(&arrangement.frontend_package_dir)
        .args([
            "build",
            "--package",
            "machine-check-gui-wasm",
            "--target=wasm32-unknown-unknown",
        ]);
    // Set the profile according to the current build profile.
    // TODO: Set the exactly same profile as our build.
    // This currently cannot be done easily due to https://github.com/rust-lang/cargo/issues/11054.
    let profile = std::env::var("PROFILE")
        .map_err(|err| anyhow!("Cannot retrieve build profile: {}", err))?;
    if profile == "debug" {
        // is used normally, no argument needed
    } else if profile == "release" {
        cargo_build.arg("--release");
    } else {
        return Err(anyhow!("Unknown build profile {}", profile));
    };

    cargo_build.arg(cargo_target_dir_arg);
    if let Err(err) = execute_command("cargo build", cargo_build) {
        let err = anyhow!("Cannot build using cargo: {}", err);
        if arrangement.postpone_build_errors {
            // propagate the error
            return Ok(Some(err));
        } else {
            return Err(err);
        }
    }

    // Prepare arguments for wasm-bindgen.
    let bindgen_out_dir_arg = create_equals_arg("out-dir", &arrangement.artifact_dir);

    // Execute wasm-bindgen to obtain the final WASM with proper bindings.
    std::env::set_current_dir(&arrangement.this_package_dir)?;
    let mut wasm_bindgen = Command::new("wasm-bindgen");
    let target_path = format!(
        "wasm32-unknown-unknown/{}/machine_check_gui_wasm.wasm",
        profile
    );

    wasm_bindgen
        .current_dir(&arrangement.this_package_dir)
        .arg("--target=web")
        .arg(bindgen_out_dir_arg)
        .arg(cargo_target_dir.join(target_path));
    execute_command("wasm-bindgen", wasm_bindgen).map_err(|err| anyhow!("Cannot generate bindings using wasm-bindgen: {}", err))?;

    // Write the frontend package directory hash to the artefact directory.

    if arrangement.prepare {
        fs::write(
            arrangement.artifact_dir.join(HASH_FILE_NAME),
            arrangement.hex_hash.clone(),
        )
        .map_err(|err| anyhow!("Cannot write frontend directory hash value: {err}"))?;
        warn!("Frontend WebAssembly prepared for deployment.")
    }
    Ok(None)
}

// --- HELPER FUNCTIONS ---

/// Make Cargo.toml paths inside a package list absolute from the given Cargo.toml path.
///
/// This function changes the working directory. Returns a map of package names to
/// corresponding paths that were patched.
fn canonicalize_paths(
    orig_toml_path: &Path,
    package_list: &mut toml_edit::Item,
) -> anyhow::Result<BTreeMap<String, String>> {
    // Set the current directory to the parent of the original TOML file.
    let Some(orig_directory) = orig_toml_path.parent() else {
        return Err(anyhow!("No parent of TOML file path {:?}", orig_toml_path));
    };

    std::env::set_current_dir(orig_directory).map_err(|err| {
        anyhow!(
            "Cannot change the current working directory to {:?}: {}",
            orig_directory,
            err
        )
    })?;

    // Coerce all path values to canonical.
    let Some(package_list) = package_list.as_table_mut() else {
        return Err(anyhow!(
            "Unexpected non-table package list {} (in {:?})",
            package_list,
            orig_toml_path
        ));
    };

    let mut package_name_paths = BTreeMap::new();

    for (package_key, package_value) in package_list.iter_mut() {
        let Some(package_value) = package_value.as_table_like_mut() else {
            if package_value.is_str() {
                // just a version number, skip
                continue;
            }

            return Err(anyhow!(
                "Unexpected non-inline-table package value for {}: '{}' (in {:?})",
                package_key,
                package_value,
                orig_toml_path
            ));
        };
        for (key, value) in package_value.iter_mut() {
            if key != "path" {
                // only path values need to be adjusted
                continue;
            }
            let package_path = value.as_str().unwrap();
            let canonical_path = fs::canonicalize(package_path)?;
            debug!(
                "Adjusting {} path to canonical {:?}",
                package_key, canonical_path
            );
            let canonical_path_string = canonical_path.to_str().ok_or_else(|| {
                anyhow!("Unexpected non-UTF-8 dependency path: {:?}", canonical_path)
            })?;

            package_name_paths.insert(package_key.to_string(), canonical_path_string.to_string());

            *value = toml_edit::Item::Value(toml_edit::Value::String(toml_edit::Formatted::new(
                canonical_path.into_os_string().into_string().unwrap(),
            )));
        }
    }
    // We are done.
    Ok(package_name_paths)
}

// Return the workspace/package path for the current working directory.
fn cargo_toml_path(workspace: bool) -> anyhow::Result<PathBuf> {
    let mut command = std::process::Command::new(env!("CARGO"));
    command.arg("locate-project");
    if workspace {
        command.arg("--workspace");
    }
    // Ensure we are using the JSON format, so that we are not in danger of trimmed spaces
    // contained in the original path (unlikely, but better safe than sorry).
    command.arg("--message-format=json");
    let output = command
        .output()
        .map_err(|err| anyhow!("Should be executable: {}", err))?;
    if !output.status.success() {
        return Err(anyhow!("Non-success status code {}", output.status));
    }

    let json_value: JsonValue = std::str::from_utf8(&output.stdout)
        .map_err(|err| anyhow!("Output should be UTF-8: {}", err))?
        .parse()
        .map_err(|err| anyhow!("Output should be JSON: {}", err))?;
    let json_object: &std::collections::HashMap<_, _> = json_value
        .get()
        .ok_or(anyhow!("Output should be an object"))?;
    let json_root = json_object
        .get("root")
        .ok_or(anyhow!("Output should have a 'root' element"))?;
    let json_root: &String = json_root
        .get()
        .ok_or(anyhow!("The 'root' element should be a string"))?;

    debug!(
        "Cargo.toml path ({}): {}",
        if workspace { "workspace" } else { "package" },
        json_root
    );

    Ok(PathBuf::from(json_root))
}

/// Copy the whole directory recursively.
fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
    fs::create_dir_all(&dst)?;
    for dir_entry in fs::read_dir(src)? {
        let dir_entry = dir_entry?;
        let file_type = dir_entry.file_type()?;
        if file_type.is_dir() {
            copy_dir_all(dir_entry.path(), dst.as_ref().join(dir_entry.file_name()))?;
        } else {
            fs::copy(dir_entry.path(), dst.as_ref().join(dir_entry.file_name()))?;
        }
    }
    Ok(())
}

/// Execute a command and returns an error on non-successful execution.
fn execute_command(name: &str, mut command: Command) -> anyhow::Result<()> {
    let output = command.output()?;
    if !output.status.success() {
        // cargo should only typically write on stderr
        Err(anyhow!(
            "{} failed, status code: {}\n --- Output:\n{}",
            name,
            output.status,
            String::from_utf8(output.stderr)?
        ))
    } else {
        debug!(
            "{} succeeded, status code: {}\n --- Output:\n{}",
            name,
            output.status,
            String::from_utf8(output.stderr)?
        );
        Ok(())
    }
}

/// Creates an argument of form '--name=path' from the argument name and path.
fn create_equals_arg(arg_name: &str, path: &Path) -> OsString {
    let mut result = OsString::from("--");
    result.push(arg_name);
    result.push("=");
    result.push(path.as_os_str());
    result
}

fn bool_env(name: &str, default: bool) -> anyhow::Result<bool> {
    println!("cargo::rerun-if-env-changed={}", name);
    let val = std::env::var_os(name);
    debug!(
        "Bool env var {} value: {:?} (default {})",
        name, val, default
    );
    let Some(val) = val else { return Ok(default) };
    // ignore case to give build scripts some leeway
    if val.eq_ignore_ascii_case("true") {
        Ok(true)
    } else if val.eq_ignore_ascii_case("false") {
        Ok(false)
    } else {
        Err(anyhow!("The environment variable '{}' should have a Boolean value (true or false ignoring case).", name))
    }
}

fn directory_hex_hash(absolute_path: String) -> anyhow::Result<String> {
    let hash_tree = merkle_hash::MerkleTree::builder(&absolute_path)
        .hash_names(false)
        .build()
        .map_err(|err| anyhow!("Cannot hash frontend package files: {}", err))?;
    let path_hash = hash_tree.root.item.hash;
    Ok(hex::encode(&path_hash))
    // this can be used for debugging which file is problematic
    /*Ok({
        let vec: Vec<String> = hash_tree
            .into_iter()
            .map(|item| format!("{}: {}", item.path.relative, hex::encode(&item.hash)))
            .collect();
        vec.join("\n")
    })*/
}

fn cargo_rerun_if_path_changed(path: &Path) -> anyhow::Result<()> {
    let path_str = path
        .to_str()
        .ok_or(anyhow!("Workspace Cargo.toml should have Unicode path"))?;
    println!("cargo::rerun-if-changed={}", path_str);
    Ok(())
}