devlyn-cli 4.2.25 → 4.2.26

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.
@@ -16,12 +16,16 @@ Linux reflink; never a copy), removes what must not carry over, and moves the re
16
16
  them: on reuse Cargo rewrites a script's own OUT_DIR in its directives, not paths elsewhere
17
17
  (aws-lc-rs's `cargo:include` into aws-lc-sys) nor inside the text it wrote (tauri's permission
18
18
  lists);
19
- - every unit of a package whose sources or generated Rust plainly compile OUT_DIR in (rather
20
- than through include!/include_str!/include_bytes!): Cargo never recompiles that path
21
- (fingerprint/dep_info.rs). This is a best-effort reading of the source, not a proof;
22
- - incremental caches.
23
-
24
- Cargo rebuilds what it then finds missing, and everything that depends on it. A path that
19
+ - every unit of a package whose crates read OUT_DIR while compiling, by rustc's dep-info record
20
+ of each env!/option_env! read in any spelling, and whose sources or generated Rust compile it
21
+ in other than through include!/include_str!/include_bytes!: Cargo never recompiles that path
22
+ (fingerprint/dep_info.rs). Telling an include from a compiled-in path is a best-effort
23
+ reading of the source, not a proof;
24
+ - incremental caches, final artifacts beside the unit directories, and outputs no fingerprint
25
+ claims.
26
+
27
+ Removal goes by unit hash across the layouts, so no Cargo command touches the clone. Cargo
28
+ rebuilds what it then finds missing, and everything that depends on it. A path that
25
29
  survives inside compiled output (a proc-macro's install name, debug maps, a native library or
26
30
  a crate that compiled a path in where the reading above does not see it) still names the
27
31
  donor's files: when the donor rebuilds or removes them, the recipient can change behavior
@@ -63,12 +67,15 @@ SNIFF_BYTES = 8192 # a NUL among the first bytes marks a binary file
63
67
  # a compiled-in path, and only the plain include!(concat!(env!("OUT_DIR"), ...)) forms, which
64
68
  # embed contents, offset one each; an env!/option_env! whose argument is not a literal counts too.
65
69
  _SPACE = r"(?:\s|//[^\n]*|/\*[\s\S]*?\*/)*"
66
- # Every quote may start a literal (a lookahead, so candidates overlap); its escapes are decoded.
67
- STRING_CANDIDATE = re.compile(r'(?="((?:[^"\\]|\\[\s\S])*?)")')
70
+ # A literal that can decode to OUT_DIR holds only its letters, \x and \u{} escapes and string
71
+ # continuations; every quote may start one (a lookahead, so candidates overlap).
72
+ STRING_CANDIDATE = re.compile(r'"(?=((?:[OUTDIR_]|\\x[0-9a-fA-F]{2}|\\u\{[0-9a-fA-F_]+\}|\\\r?\n\s*)+)")')
68
73
  ESCAPE = re.compile(r'\\(?:x([0-9a-fA-F]{2})|u\{([0-9a-fA-F_]+)\}|\n\s*|(.))')
69
- NONLITERAL_ENV = re.compile(rf'(?<![A-Za-z0-9_])(?:option_)?env{_SPACE}!{_SPACE}[(\[{{]{_SPACE}(?!r?#*")')
74
+ NONLITERAL_ENV = re.compile(rf'env{_SPACE}!{_SPACE}[(\[{{]{_SPACE}(?!r?#*")')
75
+ # Both start with a literal word, which re scans for quickly; the word's left boundary is checked
76
+ # on each match (a lookbehind first would make re try every position).
70
77
  INCLUDED_OUT_DIR = re.compile(
71
- r'(?<![A-Za-z0-9_])include(?:_str|_bytes)?\s*!\s*\(\s*concat\s*!\s*\(\s*env\s*!\s*\(\s*"OUT_DIR"\s*\)')
78
+ r'include(?:_str|_bytes)?\s*!\s*\(\s*concat\s*!\s*\(\s*env\s*!\s*\(\s*"OUT_DIR"\s*\)')
72
79
  REDIRECTS = ("CARGO_TARGET_DIR", "CARGO_BUILD_TARGET_DIR", "CARGO_BUILD_BUILD_DIR")
73
80
  RUSTC_ENV = re.compile(r"^cargo::?rustc-env=[^=]+=")
74
81
  # Runs in a child so the time limit can stop a long clone; argv: source, target.
@@ -284,18 +291,20 @@ def publish(staging_parent, staging_name, target_parent, target_name):
284
291
 
285
292
  def check_clone(staging, deadline):
286
293
  """No Git data, no special file, and no link that leads out of the clone."""
287
- for directory, dirs, files in os.walk(staging):
294
+ pending = [staging]
295
+ while pending:
288
296
  deadline.left()
289
- for name in dirs + files:
290
- path = Path(directory, name)
291
- mode = path.lstat().st_mode
292
- if name.casefold() == ".git":
293
- raise Unsafe(f"{path} is Git data")
294
- if stat.S_ISLNK(mode):
295
- if not Path(os.path.realpath(path)).is_relative_to(staging):
296
- raise Unsafe(f"{path} links out of the clone")
297
- elif not (stat.S_ISREG(mode) or stat.S_ISDIR(mode)):
298
- raise Unsafe(f"{path} is not a regular file or directory")
297
+ with os.scandir(pending.pop()) as entries:
298
+ for item in entries:
299
+ if item.name.casefold() == ".git":
300
+ raise Unsafe(f"{item.path} is Git data")
301
+ if item.is_symlink():
302
+ if not Path(os.path.realpath(item.path)).is_relative_to(staging):
303
+ raise Unsafe(f"{item.path} links out of the clone")
304
+ elif item.is_dir(follow_symlinks=False):
305
+ pending.append(item.path)
306
+ elif not item.is_file(follow_symlinks=False):
307
+ raise Unsafe(f"{item.path} is not a regular file or directory")
299
308
 
300
309
 
301
310
  def mentions(data, roots):
@@ -311,7 +320,7 @@ def is_text(path):
311
320
  return b"\0" not in stream.read(SNIFF_BYTES)
312
321
 
313
322
 
314
- def foreign_runs(staging, layout, roots, deadline):
323
+ def foreign_runs(staging, layout, roots, readers, deadline):
315
324
  """Hashes of build-script runs whose kept results name a donor root Cargo does not rewrite."""
316
325
  found = set()
317
326
  for run_dir in (staging / layout / "build").iterdir():
@@ -338,7 +347,10 @@ def foreign_runs(staging, layout, roots, deadline):
338
347
  deadline.left()
339
348
  if is_text(path):
340
349
  data = path.read_bytes()
341
- foreign = mentions(data, needles) or (name.endswith(".rs") and compiles_path(data.decode(errors="replace")))
350
+ # Generated Rust is compiled only through an include, which reads OUT_DIR.
351
+ foreign = mentions(data, needles) or (
352
+ name.endswith(".rs") and UNIT.match(run_dir.name)["name"] in readers
353
+ and compiles_path(data.decode(errors="replace")))
342
354
  if foreign:
343
355
  break
344
356
  if foreign:
@@ -357,10 +369,20 @@ def decoded(literal):
357
369
  return ESCAPE.sub(one, literal)
358
370
 
359
371
 
372
+ def at_word(text, start, prefix=""):
373
+ """Whether text[start:] begins a word (or one led by `prefix`, as option_ leads env)."""
374
+ if prefix and text.endswith(prefix, 0, start):
375
+ start -= len(prefix)
376
+ return start == 0 or not (text[start - 1].isalnum() or text[start - 1] == "_")
377
+
378
+
360
379
  def compiles_path(text):
361
380
  """Whether Rust source may compile its OUT_DIR in other than through an include macro."""
362
- literals = sum(decoded(match) == "OUT_DIR" for match in STRING_CANDIDATE.findall(text))
363
- return literals > len(INCLUDED_OUT_DIR.findall(text)) or NONLITERAL_ENV.search(text) is not None
381
+ # Without OUT_DIR spelled out or a backslash, no literal can decode to it.
382
+ literals = (sum(decoded(match) == "OUT_DIR" for match in STRING_CANDIDATE.findall(text))
383
+ if "OUT_DIR" in text or "\\" in text else 0)
384
+ included = sum(at_word(text, m.start()) for m in INCLUDED_OUT_DIR.finditer(text)) if "OUT_DIR" in text else 0
385
+ return literals > included or any(at_word(text, m.start(), "option_") for m in NONLITERAL_ENV.finditer(text))
364
386
 
365
387
 
366
388
  def compiles_out_dir(package, deadline):
@@ -415,36 +437,61 @@ def remove_unclassified(staging, layout, deadline):
415
437
  remove(item, deadline)
416
438
 
417
439
 
440
+ def crates_reading_out_dir(staging, found, crates, deadline):
441
+ """The crates among these that may have read OUT_DIR while compiling, by rustc's dep-info.
442
+
443
+ rustc writes `# env-dep:OUT_DIR=...` into a unit's deps/<crate>-<hash>.d for every
444
+ env!/option_env! read of it, whatever the spelling or macro that made it. A crate counts as
445
+ not reading it only when every kept output of it has that file and none says so. Any
446
+ hashed output counts, with or without the `lib` prefix: rlibs, rmeta, dylibs, Windows DLLs
447
+ and static libraries, test executables.
448
+ """
449
+ reading, recorded, outputs = set(), {}, {}
450
+ for layout in found:
451
+ for item in (staging / layout / "deps").iterdir():
452
+ deadline.left()
453
+ if not (match := UNIT.match(item.name)):
454
+ continue
455
+ if item.name.endswith(".d"):
456
+ if match["name"] in crates:
457
+ recorded.setdefault(match["name"], set()).add(match["hash"])
458
+ if b"\n# env-dep:OUT_DIR=" in b"\n" + item.read_bytes():
459
+ reading.add(match["name"])
460
+ continue
461
+ stem = match["name"]
462
+ for crate in {stem, stem.removeprefix("lib")} & crates:
463
+ outputs.setdefault(crate, set()).add(match["hash"])
464
+ return reading | {crate for crate, hashes in outputs.items() if hashes - recorded.get(crate, set())}
465
+
466
+
418
467
  def sanitize(work, staging, found, roots, deadline):
419
468
  for layout in found:
420
469
  remove_unclassified(staging, layout, deadline)
421
- env = {**without_redirects(), "CARGO_TARGET_DIR": str(staging)}
422
- resolved = metadata(work, deadline, env=env, deps=True)
423
- if not (Path(resolved["target_directory"]).resolve() == Path(resolved["build_directory"]).resolve() == staging.resolve()):
424
- raise Unsafe("Cargo does not resolve the staging directory as both target and build directory")
470
+ resolved = metadata(work, deadline, env=without_redirects(), deps=True)
425
471
  names = {package["name"] for package in resolved["packages"]}
426
- path_packages = sorted({package["name"] for package in resolved["packages"] if package["source"] is None})
427
- out_dir_packages = sorted({package["name"] for package in resolved["packages"] if package["source"] is not None
428
- and any("custom-build" in t["kind"] for t in package["targets"])
429
- and compiles_out_dir(package, deadline)})
472
+ path_packages = {package["name"] for package in resolved["packages"] if package["source"] is None}
473
+ # Every version of a package counts on its own; a name is removed if any version must be.
474
+ scripted = [package for package in resolved["packages"]
475
+ if package["source"] is not None and any("custom-build" in t["kind"] for t in package["targets"])]
476
+ crates = [{t["name"].replace("-", "_") for t in package["targets"] if "custom-build" not in t["kind"]}
477
+ for package in scripted]
478
+ reading = crates_reading_out_dir(staging, found, set().union(*crates), deadline)
479
+ reader_packages = [package for package, own in zip(scripted, crates) if own & reading]
480
+ readers = {package["name"] for package in reader_packages}
481
+ out_dir_packages = {package["name"] for package in reader_packages if compiles_out_dir(package, deadline)}
430
482
  removed = set()
431
483
  for layout in found:
432
- orphans = {m["hash"] for item in (staging / layout / ".fingerprint").iterdir()
433
- if (m := UNIT.match(item.name)) and m["name"] not in names}
434
- foreign = foreign_runs(staging, layout, roots, deadline)
435
- removed |= {m["name"] for item in (staging / layout / ".fingerprint").iterdir()
436
- if (m := UNIT.match(item.name)) and m["hash"] in orphans | foreign}
437
- remove_units(staging, layout, orphans | foreign, deadline)
438
- clean = path_packages + out_dir_packages
439
- if clean:
440
- base = ["cargo", "clean", "--frozen", "--target-dir", str(staging), "--profile", "dev"]
441
- for name in clean:
442
- base += ["-p", name]
443
- run(base, cwd=work, deadline=deadline, env=env) # Without --target only the host layout's outputs go.
444
- triples = [arg for layout in found[1:] for arg in ("--target", layout.parts[0])]
445
- if triples:
446
- run([*base, *triples], cwd=work, deadline=deadline, env=env)
447
- return sorted(removed | set(clean))
484
+ units = {m["hash"]: m["name"] for item in (staging / layout / ".fingerprint").iterdir()
485
+ if (m := UNIT.match(item.name))}
486
+ drop = {digest for digest, name in units.items()
487
+ if name not in names or name in path_packages or name in out_dir_packages}
488
+ drop |= foreign_runs(staging, layout, roots, readers, deadline)
489
+ removed |= {units[digest] for digest in drop if digest in units}
490
+ # Outputs without a fingerprint belong to no unit Cargo would reuse.
491
+ drop |= {m["hash"] for sub in ("build", "deps", "examples") if (staging / layout / sub).is_dir()
492
+ for item in (staging / layout / sub).iterdir() if (m := UNIT.match(item.name)) and m["hash"] not in units}
493
+ remove_units(staging, layout, drop, deadline)
494
+ return sorted(removed)
448
495
 
449
496
 
450
497
  def warm_cargo(work, anchor, staging_root, deadline):
@@ -575,34 +622,24 @@ def self_test():
575
622
  return 0 if result.wasSuccessful() else 1
576
623
 
577
624
 
578
- # Fixtures use real Git and a fake `cargo` on PATH that answers `metadata` from a JSON file
579
- # and performs `clean -p` on the layout as Cargo names it; a filesystem without cloning
580
- # copies instead, in tests only, so every rule still runs there.
625
+ # Fixtures use real Git and a fake `cargo` on PATH that answers `metadata` from a JSON file;
626
+ # a filesystem without cloning copies instead, in tests only, so every rule still runs there.
581
627
  FAKE_CARGO = r"""#!/usr/bin/env python3
582
- import json, os, pathlib, shutil, sys, time
628
+ import json, os, pathlib, sys, time
583
629
  argv = sys.argv[1:]
584
630
  fixture = json.loads(pathlib.Path(os.environ["FAKE_CARGO"]).read_text())
585
631
  with open(os.environ["FAKE_CARGO_LOG"], "a") as log:
586
632
  log.write(json.dumps(argv) + "\n")
587
633
  time.sleep(fixture.get("sleep", 0))
588
634
  target = pathlib.Path(os.environ.get("CARGO_TARGET_DIR") or os.getcwd() + "/" + fixture.get("target", "target"))
589
- if argv[0] == "metadata":
590
- found = {"target_directory": str(target), "packages": fixture["packages"] if "--no-deps" not in argv else [],
591
- "metadata": fixture.get("workspace_metadata", {"devlyn": {"warm-build-dirs": True}})}
592
- if not fixture.get("old_cargo"):
593
- found["build_directory"] = fixture.get("build_directory", str(target))
594
- print(json.dumps(found))
595
- elif argv[0] == "clean":
596
- directory = pathlib.Path(argv[argv.index("--target-dir") + 1])
597
- names = [argv[i + 1] for i, a in enumerate(argv) if a == "-p"]
598
- triples = [argv[i + 1] for i, a in enumerate(argv) if a == "--target"]
599
- for layout in [directory / t / "debug" for t in triples] or [directory / "debug"]:
600
- for name in names:
601
- for sub in (".fingerprint", "build", "deps"):
602
- for item in (layout / sub).glob("*"):
603
- stem = item.name.split(".")[0].removeprefix("lib")
604
- if stem.rsplit("-", 1)[0].replace("_", "-") == name:
605
- shutil.rmtree(item) if item.is_dir() else item.unlink()
635
+ assert argv[0] == "metadata", argv # warming never runs another cargo command
636
+ found = {"target_directory": str(target), "packages": fixture["packages"] if "--no-deps" not in argv else [],
637
+ "metadata": fixture.get("workspace_metadata", {"devlyn": {"warm-build-dirs": True}})}
638
+ if not fixture.get("old_cargo"):
639
+ found["build_directory"] = fixture.get("build_directory", str(target))
640
+ print(json.dumps(found))
641
+ # The full resolve runs while warming, after the clone: the moment to race the destination.
642
+ if "--no-deps" not in argv:
606
643
  if fixture.get("swap") and not os.path.islink("build"):
607
644
  os.rename("build", "build-moved")
608
645
  os.symlink(fixture["swap"], "build")
@@ -669,20 +706,29 @@ class WarmTests(unittest.TestCase):
669
706
 
670
707
  def write_fixture(self, **extra):
671
708
  packages = [
672
- {"name": "app", "source": None, "manifest_path": str(self.work / "Cargo.toml"), "targets": [{"kind": ["lib"]}]},
673
- {"name": "serde", "source": "registry", "manifest_path": str(self.root / "registry/serde/Cargo.toml"), "targets": [{"kind": ["lib"]}]},
674
- {"name": "ring", "source": "registry", "manifest_path": str(self.root / "registry/ring/Cargo.toml"), "targets": [{"kind": ["lib"]}, {"kind": ["custom-build"]}]},
675
- {"name": "tauri", "source": "registry", "manifest_path": str(self.root / "registry/tauri/Cargo.toml"), "targets": [{"kind": ["lib"]}, {"kind": ["custom-build"]}]},
676
- {"name": "aws-lc-rs", "source": "registry", "manifest_path": str(self.root / "registry/aws-lc-rs/Cargo.toml"), "targets": [{"kind": ["lib"]}, {"kind": ["custom-build"]}]},
677
- {"name": "baked", "source": "registry", "manifest_path": str(self.root / "registry/baked/Cargo.toml"), "targets": [{"kind": ["lib"]}, {"kind": ["custom-build"]}]},
678
- {"name": "exporter", "source": "registry", "manifest_path": str(self.root / "registry/exporter/Cargo.toml"), "targets": [{"kind": ["lib"]}, {"kind": ["custom-build"]}]},
679
- {"name": "generator", "source": "registry", "manifest_path": str(self.root / "registry/generator/Cargo.toml"), "targets": [{"kind": ["lib"]}, {"kind": ["custom-build"]}]},
709
+ {"name": "app", "source": None, "manifest_path": str(self.work / "Cargo.toml"), "targets": [{"kind": ["lib"], "name": "LIB"}]},
710
+ {"name": "serde", "source": "registry", "manifest_path": str(self.root / "registry/serde/Cargo.toml"), "targets": [{"kind": ["lib"], "name": "LIB"}]},
711
+ {"name": "ring", "source": "registry", "manifest_path": str(self.root / "registry/ring/Cargo.toml"), "targets": [{"kind": ["lib"], "name": "LIB"}, {"kind": ["custom-build"], "name": "build-script-build"}]},
712
+ {"name": "tauri", "source": "registry", "manifest_path": str(self.root / "registry/tauri/Cargo.toml"), "targets": [{"kind": ["lib"], "name": "LIB"}, {"kind": ["custom-build"], "name": "build-script-build"}]},
713
+ {"name": "aws-lc-rs", "source": "registry", "manifest_path": str(self.root / "registry/aws-lc-rs/Cargo.toml"), "targets": [{"kind": ["lib"], "name": "LIB"}, {"kind": ["custom-build"], "name": "build-script-build"}]},
714
+ {"name": "baked", "source": "registry", "manifest_path": str(self.root / "registry/baked/Cargo.toml"), "targets": [{"kind": ["lib"], "name": "LIB"}, {"kind": ["custom-build"], "name": "build-script-build"}]},
715
+ {"name": "exporter", "source": "registry", "manifest_path": str(self.root / "registry/exporter/Cargo.toml"), "targets": [{"kind": ["lib"], "name": "LIB"}, {"kind": ["custom-build"], "name": "build-script-build"}]},
716
+ {"name": "generator", "source": "registry", "manifest_path": str(self.root / "registry/generator/Cargo.toml"), "targets": [{"kind": ["lib"], "name": "LIB"}, {"kind": ["custom-build"], "name": "build-script-build"}]},
717
+ {"name": "windowsdll", "source": "registry", "manifest_path": str(self.root / "registry/windowsdll/Cargo.toml"), "targets": [{"kind": ["dylib"], "name": "LIB"}, {"kind": ["custom-build"], "name": "build-script-build"}]},
718
+ # rustc left no dep-info for its library, so nothing proves it never read OUT_DIR.
719
+ {"name": "undocumented", "source": "registry", "manifest_path": str(self.root / "registry/undocumented/Cargo.toml"), "targets": [{"kind": ["lib"], "name": "LIB"}, {"kind": ["custom-build"], "name": "build-script-build"}]},
720
+ # Two versions of one package: only 1.0 compiles OUT_DIR in.
721
+ {"name": "dual", "source": "registry", "manifest_path": str(self.root / "registry/dual-1/Cargo.toml"), "targets": [{"kind": ["lib"], "name": "LIB"}, {"kind": ["custom-build"], "name": "build-script-build"}]},
722
+ {"name": "dual", "source": "registry", "manifest_path": str(self.root / "registry/dual-2/Cargo.toml"), "targets": [{"kind": ["lib"], "name": "LIB"}, {"kind": ["custom-build"], "name": "build-script-build"}]},
680
723
  ]
724
+ for package in packages:
725
+ package["targets"] = [{**t, "name": package["name"] if t["name"] == "LIB" else t["name"]} for t in package["targets"]]
681
726
  for package in packages[1:]:
682
727
  source = Path(package["manifest_path"]).parent / "src"
683
728
  source.mkdir(parents=True, exist_ok=True)
684
- (source / "lib.rs").write_text('include!(concat!(\n env!("OUT_DIR"), "/gen.rs"));\n' if package["name"] != "baked"
685
- else 'pub const DIR: &str = env!(/* why */ "OUT_DIR");\n')
729
+ compiled = Path(package["manifest_path"]).parent.name in {"baked", "undocumented", "dual-1", "windowsdll"}
730
+ (source / "lib.rs").write_text('pub const DIR: &str = env!(/* why */ "OUT_DIR");\n' if compiled
731
+ else 'include!(concat!(\n env!("OUT_DIR"), "/gen.rs"));\n')
686
732
  self.fixture.write_text(json.dumps({"packages": packages, **extra}))
687
733
 
688
734
  def unit(self, layout, sub, name, digest, files=None):
@@ -706,6 +752,24 @@ class WarmTests(unittest.TestCase):
706
752
  self.unit(layout, ".fingerprint", name, digest)
707
753
  (deps / f"lib{name}-{digest}.rlib").write_bytes(b"rlib")
708
754
  (self.donor / layout / ".cargo-lock").write_bytes(b"")
755
+ # rustc's dep-info: which crates read OUT_DIR while compiling.
756
+ for crate, digest, reads in (("serde", "1" * 16, False), ("baked", "8" * 16, True), ("generator", "b" * 16, True),
757
+ ("ring", "c" * 16, False), ("dual", "1a" * 8, True), ("dual", "2b" * 8, True)):
758
+ record = f"# env-dep:OUT_DIR={self.donor}/{layout}/build/{crate}-x/out\n" if reads else ""
759
+ (deps / f"{crate}-{digest}.d").write_text(f"/src/{crate}/lib.rs:\n\n# env-dep:CARGO_PKG_NAME={crate}\n{record}")
760
+ self.unit(layout, ".fingerprint", crate, digest)
761
+ (deps / "libstray-dddddddddddddddd.rlib").write_bytes(b"rlib")
762
+ self.unit(layout, ".fingerprint", "undocumented", "e" * 16)
763
+ (deps / f"libundocumented-{'e' * 16}.rlib").write_bytes(b"rlib")
764
+ # A Windows-style DLL (no `lib` prefix) without dep-info, of a crate that compiles OUT_DIR in.
765
+ self.unit(layout, ".fingerprint", "windowsdll", "9a" * 8)
766
+ (deps / f"windowsdll-{'9a' * 8}.dll").write_bytes(b"dll")
767
+ for digest in ("1a" * 8, "2b" * 8):
768
+ (deps / f"libdual-{digest}.rlib").write_bytes(b"rlib")
769
+ # A run that points into the donor and has no fingerprint left.
770
+ self.unit(layout, "build", "orphanrun", "f" * 16, {
771
+ "root-output": f"{self.donor}/{layout}/build/orphanrun-{'f' * 16}/out".encode(),
772
+ "output": f"cargo:include={self.donor}/{layout}/build/aws-lc-sys-{'6' * 16}/out\n".encode()})
709
773
  (self.donor / layout / "incremental" / "app-x").mkdir(parents=True)
710
774
  # Final artifacts Cargo puts beside the unit directories, here of the unused `gone`.
711
775
  (self.donor / layout / "old-tool").write_bytes(b"bin")
@@ -743,11 +807,18 @@ class WarmTests(unittest.TestCase):
743
807
  target = self.work / "target"
744
808
  for layout in ("debug", "x86_64-pc-windows-msvc/debug"):
745
809
  kept = {p.name for p in (target / layout / ".fingerprint").iterdir()}
746
- self.assertEqual({f"serde-{'1' * 16}", f"ring-{'4' * 16}"}, kept, layout)
810
+ # generator keeps its library unit: Cargo rebuilds it after rerunning the removed run.
811
+ self.assertEqual({f"serde-{'1' * 16}", f"ring-{'4' * 16}", f"ring-{'c' * 16}", f"generator-{'b' * 16}"}, kept, layout)
812
+ self.assertFalse((target / layout / "deps" / "libstray-dddddddddddddddd.rlib").exists())
747
813
  self.assertEqual({".fingerprint", "build", "deps", "examples", ".cargo-lock"}, {p.name for p in (target / layout).iterdir()})
748
814
  self.assertEqual([], list((target / layout / "examples").iterdir()))
749
815
  self.assertTrue((target / layout / "build" / f"ring-{'4' * 16}" / "out" / "libring.a").exists())
750
- self.assertEqual(["app", "aws-lc-rs", "baked", "exporter", "generator", "gone", "tauri"], result["removed"])
816
+ self.assertEqual(["app", "aws-lc-rs", "baked", "dual", "exporter", "generator", "gone", "tauri", "undocumented",
817
+ "windowsdll"],
818
+ result["removed"])
819
+ for layout in ("debug", "x86_64-pc-windows-msvc/debug"):
820
+ self.assertFalse((target / layout / "build" / f"orphanrun-{'f' * 16}").exists())
821
+ self.assertFalse(any((target / layout / "deps").glob("libdual-*")))
751
822
  self.assertEqual(str(self.donor), result["donor"])
752
823
  self.assertEqual(before, self.snapshot(self.donor))
753
824
  self.assert_nothing_left()
@@ -876,7 +947,7 @@ class WarmTests(unittest.TestCase):
876
947
  'option_env!("OUT_DIR")', 'myinclude!(concat!(env!("OUT_DIR"), "/a.rs"))', 'env![ "OUT_DIR" ]',
877
948
  'env/**/!("OUT_DIR")', 'env!(/* why */ "OUT_DIR")', 'env!("OUT\\x5fDIR")', 'env!("\\u{4f}UT_DIR")',
878
949
  'use std::env as e; e!("OUT_DIR")', 'env!(concat!("OUT", "_DIR"))', 'env!(// note\n"OUT_DIR")',
879
- 'env!("\\u{4_f}UT_DIR")', 'env!("OUT_\\\n DIR")', 'let a = \'"\'; env!("OUT_DIR")',
950
+ 'env!("\\u{4_f}UT_DIR")', 'env!("\\u{0_0_0_0_4_f}UT_DIR")', 'env!("OUT_\\\n DIR")', 'let a = \'"\'; env!("OUT_DIR")',
880
951
  'include!(concat!(env!(/* c */ "OUT_DIR"), "/a.rs"))']
881
952
  for text in compiled:
882
953
  self.assertTrue(compiles_path(text), text)
package/package.json CHANGED
@@ -1,14 +1,20 @@
1
1
  {
2
2
  "name": "devlyn-cli",
3
- "version": "4.2.25",
3
+ "version": "4.2.26",
4
4
  "description": "Loop & harness engineering for AI coding agents (Claude Code, Codex, and any AGENTS.md-driven agent) — core engineering principles for CLAUDE.md/AGENTS.md and a loop designer that splits one intent into tasks agents drain hands-free",
5
5
  "homepage": "https://github.com/fysoul17/devlyn-cli#readme",
6
6
  "bin": {
7
7
  "devlyn": "bin/devlyn.js"
8
8
  },
9
+ "scripts": {
10
+ "prepack": "node -e \"require('./bin/release').buildRelease()\""
11
+ },
9
12
  "files": [
10
13
  "bin",
11
14
  "config",
15
+ "harness",
16
+ "native",
17
+ "release.json",
12
18
  "!config/skills/preflight-workspace",
13
19
  "!config/skills/preflight-workspace/**",
14
20
  "!config/skills/devlyn:ideate-workspace",