supermachine 0.7.41

Run any OCI/Docker image as a hardware-isolated microVM on macOS HVF (Linux KVM and Windows WHP in progress). Single library API, zero flags for the common case, sub-100 ms cold-restore from snapshot.
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
//! Locate the runtime assets that supermachine needs to start a
//! microVM: the Linux kernel image, the in-VM init shim source +
//! prebuilt binary, and the HVF entitlements plist.
//!
//! The CLI subcommands `supermachine bundle`, `supermachine
//! codesign`, `supermachine assets-path`, and `supermachine
//! entitlements-path` use these helpers. Embedders shipping a
//! `.app` bundle should call [`AssetPaths::from_app_bundle`] with
//! the `Contents/Resources/` dir of their bundle.
//!
//! ## Discovery order
//!
//! 1. `$SUPERMACHINE_ASSETS_DIR` if set (explicit override).
//! 2. The directory containing the running binary's executable (so
//!    a `.app/Contents/MacOS/your-app` finds assets in
//!    `Contents/Resources/`).
//! 3. `$HOME/.local/share/supermachine/` (release-tarball install).
//! 4. The dev-tree path `crates/supermachine/{kernel,oci,...}`
//!    relative to the binary's ancestors (cargo workspace dev).
//! 5. **Bundled fallback** — extract the kernel + init shim that
//!    rode inside this binary (via the `supermachine-kernel`
//!    crate's `KERNEL_BYTES` / `INIT_OCI_BYTES` constants) into
//!    `$XDG_DATA_HOME/supermachine/v{VERSION}/` and return that.
//!    First call writes the files; subsequent calls reuse them.
//!    This is what makes `cargo install supermachine` work with
//!    zero manual setup — the bytes are always available, even on
//!    a host that's never seen a supermachine release tarball or
//!    dev tree.
//!
//! The first match wins. Each helper returns `Option<PathBuf>` so
//! callers can compose their own fallback strategy.

use std::path::{Path, PathBuf};
use std::sync::OnceLock;

/// Bundled HVF entitlements plist content. Embedded at compile
/// time so the CLI's `codesign` subcommand and `cargo-supermachine`
/// plugin work without an on-disk plist file. Customers signing
/// their own .app for distribution should write this to a temp
/// file and pass it to `codesign --entitlements`.
pub const ENTITLEMENTS_PLIST: &str =
    include_str!("../entitlements.plist");

/// Where supermachine looks for runtime assets, in priority order.
#[derive(Debug, Clone)]
pub struct AssetPaths {
    /// Path to the Linux kernel image. Required to start a VM.
    pub kernel: Option<PathBuf>,
    /// Path to the prebuilt in-VM init binary, if any. The CLI
    /// builds this from `oci/init-oci.c` on first bake if absent;
    /// release-tarball installs ship it pre-built.
    pub init_oci_bin: Option<PathBuf>,
    /// Path to the in-VM init source. Used as a fallback when the
    /// prebuilt binary isn't shipped (the CLI compiles it via zig).
    pub init_oci_src: Option<PathBuf>,
    /// Path to the prebuilt in-VM `supermachine-agent` binary
    /// (statically-linked aarch64-musl). Bake copies it into the
    /// delta layer at `/supermachine-agent`; init-oci forks and
    /// exec's it post-pivot for the in-guest exec / control RPCs.
    pub supermachine_agent: Option<PathBuf>,
    /// Path to the prebuilt `smpark.ko` kernel module
    /// (aarch64, vermagic-matched to [`kernel`]). Bake stages it
    /// into the initramfs at `/supermachine-smpark.ko`; init-oci
    /// loads it via `finit_module` BEFORE the rootfs pivot.
    /// Required for `vcpus > 1` reliability — see
    /// `docs/design/multi-vcpu-shipped-2026-05-18.md`.
    pub smpark_ko: Option<PathBuf>,
}

impl AssetPaths {
    /// Auto-discover assets from the standard search paths.
    pub fn discover() -> Self {
        // Honored: $SUPERMACHINE_ASSETS_DIR
        if let Some(dir) = std::env::var_os("SUPERMACHINE_ASSETS_DIR") {
            let dir = PathBuf::from(dir);
            return Self::from_dir(&dir);
        }

        let exe = std::env::current_exe().ok();

        // .app bundle: <prefix>/Contents/MacOS/your-app  →  <prefix>/Contents/Resources/
        if let Some(exe) = exe.as_deref() {
            if let Some(macos_dir) = exe.parent() {
                if macos_dir.file_name().and_then(|s| s.to_str()) == Some("MacOS") {
                    if let Some(contents) = macos_dir.parent() {
                        let res = contents.join("Resources");
                        let probe = Self::from_dir(&res);
                        if probe.kernel.is_some() {
                            return probe;
                        }
                    }
                }
            }
        }

        // Tarball install: <prefix>/bin/supermachine + <prefix>/share/supermachine/
        if let Some(exe) = exe.as_deref() {
            for ancestor in exe.ancestors() {
                let share = ancestor.join("share/supermachine");
                if share.join("kernel").is_file() {
                    return Self::from_dir(&share);
                }
            }
        }

        // Dev tree: walk ancestors of the binary. Kernel lives in
        // the `supermachine-kernel` data crate; init shim sources
        // and prebuilt binaries live next to the main crate's `oci/`.
        if let Some(exe) = exe.as_deref() {
            for ancestor in exe.ancestors() {
                let kernel_crate = ancestor.join("crates/supermachine-kernel");
                let main_crate = ancestor.join("crates/supermachine");
                let agent_crate = ancestor.join("crates/supermachine-guest-agent");
                if kernel_crate.join("kernel").is_file() {
                    return Self {
                        kernel: Some(kernel_crate.join("kernel")),
                        init_oci_bin: Some(main_crate.join("oci/init-oci"))
                            .filter(|p| p.is_file()),
                        init_oci_src: Some(main_crate.join("oci/init-oci.c"))
                            .filter(|p| p.is_file()),
                        supermachine_agent: Some(agent_crate.join(
                            "target/aarch64-unknown-linux-musl/release/supermachine-agent",
                        ))
                        .filter(|p| p.is_file()),
                        smpark_ko: Some(
                            main_crate.join("oci/supermachine-smpark.ko"),
                        )
                        .filter(|p| p.is_file()),
                    };
                }
            }
        }

        // Final fallback: extract the bundled bytes (rode inside
        // this binary via supermachine-kernel) into a per-user
        // data dir. This is the path that makes
        // `cargo install supermachine` produce a working CLI with
        // zero manual setup.
        if let Some(dir) = ensure_bundled_extracted() {
            let probe = Self::from_dir(&dir);
            if probe.kernel.is_some() {
                return probe;
            }
        }

        Self {
            kernel: None,
            init_oci_bin: None,
            init_oci_src: None,
            supermachine_agent: None,
            smpark_ko: None,
        }
    }

    /// Locate assets relative to a single directory (used by the
    /// `.app/Contents/Resources/` and tarball-install layouts).
    pub fn from_dir(dir: &Path) -> Self {
        Self {
            kernel: Some(dir.join("kernel")).filter(|p| p.is_file()),
            init_oci_bin: Some(dir.join("init-oci")).filter(|p| p.is_file()),
            init_oci_src: Some(dir.join("init-oci.c")).filter(|p| p.is_file()),
            supermachine_agent: Some(dir.join("supermachine-agent"))
                .filter(|p| p.is_file()),
            // The bundled-extract path writes the module as
            // `supermachine-smpark.ko` (the same filename
            // `build_initramfs` looks for next to init-oci). Older
            // tarball/app layouts that pre-date 0.7.26 won't have
            // this file; `.filter(is_file)` keeps it as None and
            // multi-vCPU falls back to the GIC-only path.
            smpark_ko: Some(dir.join("supermachine-smpark.ko"))
                .filter(|p| p.is_file()),
        }
    }

    /// Locate assets inside a macOS `.app` bundle. Pass the path to
    /// the `.app` itself (not its `Contents/Resources/`).
    pub fn from_app_bundle(app: &Path) -> Self {
        Self::from_dir(&app.join("Contents/Resources"))
    }
}

/// Extract the bundled kernel + init-oci bytes (linked into this
/// binary via the `supermachine-kernel` crate) to a per-user data
/// dir, returning the dir path on success. First invocation writes
/// the files; subsequent invocations short-circuit and return the
/// same dir.
///
/// Layout:
///
/// ```text
/// $XDG_DATA_HOME/supermachine/v{VERSION}/
///     kernel
///     init-oci
/// ```
///
/// Falls back to `$HOME/.local/share/...` when XDG isn't set, and
/// gives up (returns `None`) when neither HOME nor XDG_DATA_HOME
/// resolves — caller will then surface a "kernel not found" error
/// the same way it would on any other host without assets.
///
/// Errors writing the files (permission denied, disk full, ...)
/// are swallowed: we return `None` so the caller's error path
/// triggers cleanly. The next discover() attempt will retry.
fn ensure_bundled_extracted() -> Option<PathBuf> {
    let dir = bundled_assets_dir()?;
    let kernel_dst = dir.join("kernel");
    let init_dst = dir.join("init-oci");
    let agent_dst = dir.join("supermachine-agent");
    let smpark_dst = dir.join("supermachine-smpark.ko");
    let kernel_hash_dst = dir.join("kernel.hash");
    let init_hash_dst = dir.join("init-oci.hash");
    let agent_hash_dst = dir.join("supermachine-agent.hash");
    let smpark_hash_dst = dir.join("supermachine-smpark.hash");

    let want_kernel = kernel_bytes_hash();
    let want_init = init_oci_bytes_hash();
    let want_agent = supermachine_agent_bytes_hash();
    let want_smpark = smpark_ko_bytes_hash();

    // Cache hit: all four binaries AND their sibling .hash files
    // exist AND every hash matches this process's expectation.
    // Missing .hash file (old install) or mismatching hash (bytes
    // changed without a version bump — happens with path deps in
    // dev workflows) is treated as a miss and forces re-extract,
    // so a rebuilt kernel.xz isn't silently shadowed by a stale
    // cached copy.
    if kernel_dst.is_file()
        && init_dst.is_file()
        && agent_dst.is_file()
        && smpark_dst.is_file()
        && read_hash_file(&kernel_hash_dst).as_deref() == Some(want_kernel)
        && read_hash_file(&init_hash_dst).as_deref() == Some(want_init)
        && read_hash_file(&agent_hash_dst).as_deref() == Some(want_agent)
        && read_hash_file(&smpark_hash_dst).as_deref() == Some(want_smpark)
    {
        return Some(dir);
    }

    if std::fs::create_dir_all(&dir).is_err() {
        return None;
    }

    // Re-extract any asset whose on-disk hash doesn't match the
    // current process's bytes (or that's missing entirely). We
    // write the binary first, then the .hash file, so a crash
    // mid-extract leaves the .hash stale-or-missing and the next
    // run treats it as a miss and retries — never the other way
    // around (which could mask a torn binary as "valid").
    if !kernel_dst.is_file()
        || read_hash_file(&kernel_hash_dst).as_deref() != Some(want_kernel)
    {
        if !atomic_write(&dir, &kernel_dst, "kernel.partial", |tmp| {
            std::fs::write(tmp, supermachine_kernel::KERNEL_BYTES)
        }) {
            return None;
        }
        if !atomic_write_str(&dir, &kernel_hash_dst, "kernel.hash.partial", want_kernel)
        {
            return None;
        }
    }
    if !init_dst.is_file()
        || read_hash_file(&init_hash_dst).as_deref() != Some(want_init)
    {
        if !atomic_write(&dir, &init_dst, "init-oci.partial", |tmp| {
            supermachine_kernel::extract_init_oci_to(tmp)
        }) {
            return None;
        }
        if !atomic_write_str(&dir, &init_hash_dst, "init-oci.hash.partial", want_init)
        {
            return None;
        }
    }
    if !agent_dst.is_file()
        || read_hash_file(&agent_hash_dst).as_deref() != Some(want_agent)
    {
        if !atomic_write(&dir, &agent_dst, "supermachine-agent.partial", |tmp| {
            supermachine_kernel::extract_supermachine_agent_to(tmp)
        }) {
            return None;
        }
        if !atomic_write_str(
            &dir,
            &agent_hash_dst,
            "supermachine-agent.hash.partial",
            want_agent,
        ) {
            return None;
        }
    }
    if !smpark_dst.is_file()
        || read_hash_file(&smpark_hash_dst).as_deref() != Some(want_smpark)
    {
        if !atomic_write(&dir, &smpark_dst, "supermachine-smpark.ko.partial", |tmp| {
            supermachine_kernel::extract_smpark_ko_to(tmp)
        }) {
            return None;
        }
        if !atomic_write_str(
            &dir,
            &smpark_hash_dst,
            "supermachine-smpark.hash.partial",
            want_smpark,
        ) {
            return None;
        }
    }
    Some(dir)
}

/// Atomic write helper: stage via a sibling `.partial` file then
/// rename into place. The writer closure gets the tmp path. Two
/// parallel discover() calls (e.g. CLI + library in the same
/// process tree) won't tear a half-written file. Returns true on
/// success.
fn atomic_write<F>(dir: &Path, dst: &Path, tmp_name: &str, write: F) -> bool
where
    F: FnOnce(&Path) -> std::io::Result<()>,
{
    let tmp = dir.join(tmp_name);
    if write(&tmp).is_err() {
        let _ = std::fs::remove_file(&tmp);
        return false;
    }
    if std::fs::rename(&tmp, dst).is_err() {
        let _ = std::fs::remove_file(&tmp);
        return false;
    }
    true
}

fn atomic_write_str(dir: &Path, dst: &Path, tmp_name: &str, contents: &str) -> bool {
    atomic_write(dir, dst, tmp_name, |tmp| std::fs::write(tmp, contents))
}

/// Read a sibling `.hash` file and return its contents as a
/// trimmed string. Returns `None` if the file is missing,
/// unreadable, or longer than a sane bound — the caller treats
/// any of those as a cache miss and re-extracts. The size cap
/// keeps a corrupted-or-malicious cache dir from making us slurp
/// gigabytes.
fn read_hash_file(path: &Path) -> Option<String> {
    let bytes = std::fs::read(path).ok()?;
    if bytes.len() > 128 {
        return None;
    }
    Some(String::from_utf8(bytes).ok()?.trim().to_owned())
}

/// 12-hex-char SHA-256 prefix of `KERNEL_BYTES`. Hashed once per
/// process and cached in a `OnceLock` so multiple `discover()`
/// calls don't re-hash ~29 MiB every time.
fn kernel_bytes_hash() -> &'static str {
    static H: OnceLock<String> = OnceLock::new();
    H.get_or_init(|| short_sha256(supermachine_kernel::KERNEL_BYTES))
}

fn init_oci_bytes_hash() -> &'static str {
    static H: OnceLock<String> = OnceLock::new();
    H.get_or_init(|| short_sha256(supermachine_kernel::INIT_OCI_BYTES))
}

fn supermachine_agent_bytes_hash() -> &'static str {
    static H: OnceLock<String> = OnceLock::new();
    H.get_or_init(|| short_sha256(supermachine_kernel::SUPERMACHINE_AGENT_BYTES))
}

fn smpark_ko_bytes_hash() -> &'static str {
    static H: OnceLock<String> = OnceLock::new();
    H.get_or_init(|| short_sha256(supermachine_kernel::SMPARK_KO_BYTES))
}

/// SHA-256 of `bytes`, hex-encoded, truncated to 12 chars
/// (48 bits). Enough to distinguish accidental rebuilds of the
/// same asset — not a security boundary; the cache dir is
/// already user-writable, so a hostile actor can swap files
/// regardless.
fn short_sha256(bytes: &[u8]) -> String {
    let digest = ring::digest::digest(&ring::digest::SHA256, bytes);
    let mut s = String::with_capacity(12);
    for b in &digest.as_ref()[..6] {
        s.push_str(&format!("{:02x}", b));
    }
    s
}

/// Copy `supermachine-worker` from its discovered source path
/// (`@supermachine/core-darwin-arm64/supermachine-worker`,
/// `~/.cargo/bin/supermachine-worker`, …) into the per-version
/// user-data dir and return the user-data path. The library spawns
/// FROM THE COPY, and the codesign autopilot signs that copy — so
/// `node_modules` (or `~/.cargo/bin`, depending on install method)
/// is never mutated.
///
/// Why we need this: prior versions called `codesign --force` on
/// the npm-installed file directly. That:
///   1. Mutates a path the package manager considers immutable.
///   2. Triggers macOS `codesign` to write a `.cstemp` next to it.
///      If the user has chflags-locked the worker (a real
///      diagnostic scenario from the field), codesign can't
///      complete the atomic rename and the `.cstemp` is left
///      with `uchg` set — permanently breaking every future
///      `codesign --force` with "internal error in Code Signing
///      subsystem". `npm install` doesn't touch `.cstemp`, so the
///      breakage survives reinstalls.
///   3. Bumps mtime on the npm file every fresh install →
///      file-watchers (Watchman, VSCode reloaders) see churn.
///
/// Copying to a user-writable scratch dir isolates all that.
///
/// Idempotent on a content hash of the source: re-runs are O(1)
/// `stat` calls. A source change (npm upgrade, fresh
/// `cargo install`) bumps the hash → copy re-runs.
pub fn ensure_worker_in_user_dir(source: &Path) -> Result<PathBuf, String> {
    let dir = bundled_assets_dir().ok_or_else(|| {
        format!(
            "no $XDG_DATA_HOME or $HOME to extract worker into; \
             set $SUPERMACHINE_WORKER_BIN to a user-writable path \
             holding a v{} worker binary instead",
            env!("CARGO_PKG_VERSION")
        )
    })?;
    ensure_worker_in_dir(source, &dir)
}

/// Inner copy + atomic-rename + hash-sentinel logic, factored out
/// from [`ensure_worker_in_user_dir`] so unit tests can drive an
/// explicit `dir` without depending on `$XDG_DATA_HOME`/`$HOME`
/// process-globals (which would race other parallel tests).
pub(crate) fn ensure_worker_in_dir(source: &Path, dir: &Path) -> Result<PathBuf, String> {
    let dst = dir.join("supermachine-worker");
    let hash_dst = dir.join("supermachine-worker.hash");
    let source_hash = short_sha256_file(source).map_err(|e| {
        format!("hash worker source {}: {e}", source.display())
    })?;

    // Cache hit: the user-dir worker exists AND its sibling
    // .hash matches the source's content hash. Same invalidation
    // semantics as the kernel/init-oci/agent bundled-extract code
    // — a source change forces a re-copy.
    if dst.is_file() && read_hash_file(&hash_dst).as_deref() == Some(source_hash.as_str()) {
        return Ok(dst);
    }

    std::fs::create_dir_all(dir).map_err(|e| {
        format!("mkdir -p {}: {e}", dir.display())
    })?;

    // Atomic write: copy to a sibling `.partial`, set executable,
    // rename into place. Same shape as the kernel/init-oci/agent
    // extraction below. Critical detail: we must NEVER leave the
    // dst path occupied by a partially-copied binary — codesign
    // would then sign 2 MB of garbage and the next bake would
    // crash on entry.
    let partial = dir.join("supermachine-worker.partial");
    let _ = std::fs::remove_file(&partial);
    std::fs::copy(source, &partial).map_err(|e| {
        format!(
            "copy {} -> {}: {e}",
            source.display(),
            partial.display()
        )
    })?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mut perms = std::fs::metadata(&partial)
            .map_err(|e| format!("stat partial worker: {e}"))?
            .permissions();
        perms.set_mode(0o755);
        std::fs::set_permissions(&partial, perms)
            .map_err(|e| format!("chmod partial worker: {e}"))?;
    }
    std::fs::rename(&partial, &dst).map_err(|e| {
        let _ = std::fs::remove_file(&partial);
        format!("rename {} -> {}: {e}", partial.display(), dst.display())
    })?;
    if !atomic_write_str(
        dir,
        &hash_dst,
        "supermachine-worker.hash.partial",
        &source_hash,
    ) {
        return Err(format!(
            "write {}: failed (disk full? perms?)",
            hash_dst.display()
        ));
    }
    Ok(dst)
}

/// Like [`short_sha256`] but for a file path; reads up to 64 MiB.
/// Worker binaries are ~2 MiB so the cap is generous.
fn short_sha256_file(path: &Path) -> Result<String, std::io::Error> {
    let bytes = std::fs::read(path)?;
    Ok(short_sha256(&bytes))
}

/// Resolve `$XDG_DATA_HOME/supermachine/v{VERSION}/`, falling back
/// to `$HOME/.local/share/supermachine/v{VERSION}/`. Versioned so
/// upgrading supermachine doesn't reuse stale assets — each crate
/// version gets its own dir.
fn bundled_assets_dir() -> Option<PathBuf> {
    let base = if let Some(d) = std::env::var_os("XDG_DATA_HOME") {
        PathBuf::from(d)
    } else if let Some(h) = std::env::var_os("HOME") {
        PathBuf::from(h).join(".local/share")
    } else {
        return None;
    };
    Some(
        base.join("supermachine")
            .join(format!("v{}", env!("CARGO_PKG_VERSION"))),
    )
}

#[cfg(test)]
mod ensure_worker_in_dir_tests {
    //! 0.7.28 user-dir worker copy: atomic write, hash-based cache
    //! invalidation, idempotency. Tests use an explicit dir so they
    //! don't race on the `$XDG_DATA_HOME`/`$HOME` process-global.
    use super::{ensure_worker_in_dir, short_sha256_file};
    use std::io::Write;
    use std::os::unix::fs::PermissionsExt;
    use std::path::PathBuf;
    use std::sync::atomic::{AtomicU64, Ordering};

    static TEST_ID: AtomicU64 = AtomicU64::new(0);

    fn unique_tmp_dir(label: &str) -> PathBuf {
        let id = TEST_ID.fetch_add(1, Ordering::Relaxed);
        let p = std::env::temp_dir().join(format!(
            "supermachine-userdir-test-{}-{label}-{id}",
            std::process::id()
        ));
        let _ = std::fs::remove_dir_all(&p);
        p
    }

    fn make_worker(path: &std::path::Path, contents: &[u8]) {
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent).expect("mkdir parent");
        }
        let mut f = std::fs::File::create(path).expect("create worker source");
        f.write_all(contents).expect("write worker source");
    }

    #[test]
    fn first_call_copies_source_and_writes_hash_sentinel() {
        let src_dir = unique_tmp_dir("first-src");
        let dst_dir = unique_tmp_dir("first-dst");
        let src = src_dir.join("worker-source");
        let payload = b"#!/bin/sh\necho hi\n";
        make_worker(&src, payload);

        let dst = ensure_worker_in_dir(&src, &dst_dir).expect("first call");
        assert_eq!(dst, dst_dir.join("supermachine-worker"));
        assert!(dst.exists(), "dst worker should be present");

        // Content matches.
        assert_eq!(std::fs::read(&dst).unwrap(), payload);

        // Hash sentinel written + correct content.
        let hash_file = dst_dir.join("supermachine-worker.hash");
        assert!(hash_file.exists());
        let want = short_sha256_file(&src).unwrap();
        let got = std::fs::read_to_string(&hash_file).unwrap();
        assert_eq!(got.trim(), want);

        let _ = std::fs::remove_dir_all(&src_dir);
        let _ = std::fs::remove_dir_all(&dst_dir);
    }

    #[test]
    fn second_call_on_unchanged_source_is_a_cache_hit() {
        let src_dir = unique_tmp_dir("hit-src");
        let dst_dir = unique_tmp_dir("hit-dst");
        let src = src_dir.join("worker-source");
        make_worker(&src, b"static contents");

        let dst1 = ensure_worker_in_dir(&src, &dst_dir).unwrap();
        // Capture the mtime of dst1.
        let mtime1 = std::fs::metadata(&dst1).unwrap().modified().unwrap();
        // Sleep enough that mtime would observably differ on a
        // re-copy (HFS+/APFS mtime resolution is 1 ns but file_time
        // resolution differs by FS — a 100 ms gap is safe).
        std::thread::sleep(std::time::Duration::from_millis(150));

        let dst2 = ensure_worker_in_dir(&src, &dst_dir).unwrap();
        assert_eq!(dst1, dst2);
        let mtime2 = std::fs::metadata(&dst2).unwrap().modified().unwrap();
        assert_eq!(
            mtime1, mtime2,
            "cache hit must NOT rewrite the file (mtime would change)"
        );

        let _ = std::fs::remove_dir_all(&src_dir);
        let _ = std::fs::remove_dir_all(&dst_dir);
    }

    #[test]
    fn source_change_invalidates_cache_and_recopies() {
        let src_dir = unique_tmp_dir("inv-src");
        let dst_dir = unique_tmp_dir("inv-dst");
        let src = src_dir.join("worker-source");

        make_worker(&src, b"version 1 contents");
        let dst1 = ensure_worker_in_dir(&src, &dst_dir).unwrap();
        let contents1 = std::fs::read(&dst1).unwrap();

        // Source content changes — emulates `npm install` of a new
        // version landing a fresh worker bin at the same path.
        make_worker(&src, b"version 2 contents - different");
        let dst2 = ensure_worker_in_dir(&src, &dst_dir).unwrap();
        let contents2 = std::fs::read(&dst2).unwrap();

        assert_eq!(dst1, dst2);
        assert_ne!(
            contents1, contents2,
            "source content change must invalidate the cache and re-copy"
        );
        assert_eq!(
            contents2, b"version 2 contents - different",
            "dst must reflect new source content"
        );

        let _ = std::fs::remove_dir_all(&src_dir);
        let _ = std::fs::remove_dir_all(&dst_dir);
    }

    #[test]
    fn copied_worker_is_executable() {
        // codesign needs the worker to be `0755` (or anything with
        // user-x bit) to set its signature — and HVF then needs it
        // to actually launch. Verify mode is set.
        let src_dir = unique_tmp_dir("exec-src");
        let dst_dir = unique_tmp_dir("exec-dst");
        let src = src_dir.join("worker-source");
        make_worker(&src, b"executable test");
        // Source might be 0644; what matters is the destination.
        let _ = std::fs::set_permissions(&src, std::fs::Permissions::from_mode(0o644));

        let dst = ensure_worker_in_dir(&src, &dst_dir).unwrap();
        let mode = std::fs::metadata(&dst).unwrap().permissions().mode() & 0o777;
        assert_eq!(mode, 0o755, "dst worker must be 0755 regardless of source mode");

        let _ = std::fs::remove_dir_all(&src_dir);
        let _ = std::fs::remove_dir_all(&dst_dir);
    }

    #[test]
    fn missing_source_returns_error_not_panic() {
        let dst_dir = unique_tmp_dir("missing-dst");
        let result = ensure_worker_in_dir(
            std::path::Path::new("/does/not/exist/never/at/all"),
            &dst_dir,
        );
        assert!(result.is_err(), "missing source should error");
        let msg = result.err().unwrap();
        assert!(
            msg.contains("hash worker source") || msg.contains("copy "),
            "error should be actionable, got: {msg}"
        );

        let _ = std::fs::remove_dir_all(&dst_dir);
    }

    #[test]
    fn dst_dir_created_on_demand() {
        // dst_dir doesn't exist yet — ensure_worker_in_dir should
        // mkdir -p, not fail.
        let src_dir = unique_tmp_dir("mkdir-src");
        // Note: dst_dir is a *nested* path that doesn't exist.
        let dst_dir = unique_tmp_dir("mkdir-dst").join("nested/deeper/v0.7.28");
        let src = src_dir.join("worker-source");
        make_worker(&src, b"mkdir test");

        let dst = ensure_worker_in_dir(&src, &dst_dir).unwrap();
        assert!(dst.exists(), "ensure_worker_in_dir must mkdir -p dst dir");

        let _ = std::fs::remove_dir_all(&src_dir);
        let _ = std::fs::remove_dir_all(dst_dir.ancestors().nth(2).unwrap());
    }

    #[test]
    fn stale_partial_file_doesnt_block_copy() {
        // Simulate the failure mode: a previous run crashed
        // mid-copy, leaving `supermachine-worker.partial` behind.
        // Next call should clean it up and proceed.
        let src_dir = unique_tmp_dir("stale-src");
        let dst_dir = unique_tmp_dir("stale-dst");
        let src = src_dir.join("worker-source");
        make_worker(&src, b"stale-partial test");

        std::fs::create_dir_all(&dst_dir).unwrap();
        let stale_partial = dst_dir.join("supermachine-worker.partial");
        std::fs::write(&stale_partial, b"garbage from prior aborted run").unwrap();

        let dst = ensure_worker_in_dir(&src, &dst_dir).unwrap();
        assert_eq!(std::fs::read(&dst).unwrap(), b"stale-partial test");
        assert!(!stale_partial.exists(), "stale .partial must be cleaned up");

        let _ = std::fs::remove_dir_all(&src_dir);
        let _ = std::fs::remove_dir_all(&dst_dir);
    }
}