Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 51 additions & 8 deletions adopt-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,10 @@
tree_digest,
)
from shape_materialize import ( # noqa: E402
ADOPT_MAKEFILE_BLOCK, PLACEHOLDER_RE, SHAPE_REPOSITORY, SYMLINK_HOPS,
PLACEHOLDER_RE, SHAPE_REPOSITORY, SYMLINK_HOPS,
TEMPLATED, TOO_MANY_SYMLINKS, CommandFailed, Materialized,
assembly_root_paths, collision_follow_up, copy_tree, default_reference,
adopt_makefile_block, assembly_root_paths, collision_follow_up, copy_tree,
default_reference,
election_date, env_commit, escape_what_stdout_cannot_encode,
git_init_commit, materialize_assembly_root, naming_block, render, run,
template_files, write_lf,
Expand Down Expand Up @@ -1996,19 +1997,50 @@ def cross_leg_references(source: "Source", entries: list["Entry"],
# DOES read it under twenty of them is how a follow-up list stops
# being read at all.
hits.sort(key=lambda h: ("fixture" in h or h.startswith("tests/"), h))
across = spec_leg_from_code_leg(spec_path, code_path)
for hit in hits[:8]:
shown = "".join(map(_spelled_character, hit))
out.append(
f"{code_path}/{shown} names `{named}`, which moves to the "
f"spec leg: it must read across the assembly root — "
f"`../{spec_path}/{named}` relative to `{code_path}/` — or "
f"`{across}/{named}` relative to `{code_path}/` — or "
"take the path from the environment")
if len(hits) > 8:
out.append(f"… and {len(hits) - 8} more file(s) under the code leg "
f"naming `{named}`")
return out


def spec_leg_from_code_leg(spec_path: str, code_path: str) -> str:
"""The spec leg's mount, `spec_path`, as a path relative to the code
leg's, `code_path`: `../spec` for the defaults, `../spec` for
`legs/spec` beside `legs/code`, and `../../spec` for `spec` beside
`impl/code`.

ONE `..` PER SEGMENT OF THE CODE LEG'S PATH BELOW WHAT THE TWO SHARE
(#522, auA-13). The follow-up wrote `../<spec_path>`, which is right only
while the code leg sits in the root: with `--spec-path legs/spec
--code-path legs/code` it said `../legs/spec/contracts/` "relative to
`legs/code/`", a path that leads to `legs/legs/spec`.

Both are canonical mount paths, asked by `plan` before it writes a
follow-up and by `execute` before it mounts a leg: `/` between
segments, and no segment empty, `.` or `..`. So the segments are
compared as text, and nothing is resolved against a disk. Two segments
that differ only in case are two segments here, and the comparison stops
at the first of them: the path climbs one level more than a disk that
folds case needs for every segment from there to the end of what such a
disk would call shared (`A/B/spec` beside `a/b/code` climbs two more,
and so does `A/b/spec`), and still leads to the same directory.
"""
spec, code = spec_path.split("/"), code_path.split("/")
shared = 0
while (shared < min(len(spec), len(code))
and spec[shared] == code[shared]):
shared += 1
return "/".join([".."] * (len(code) - shared) + spec[shared:])


def _search_by_hand(named: str, why: str) -> str:
"""The follow-up for a spec directory git did not search for: the
reason, and what to look for instead."""
Expand Down Expand Up @@ -2045,15 +2077,24 @@ def follow_ups_for(source: "Source", entries: list["Entry"], names: dict,
and pushes before the split, extracted or, with no path, seeded from a
template. "Seeded", said of both, named an extracted leg by the word kept
for a template's.

The Makefile follow-up names the key too, for the spec leg's mount: it
names `legs.spec_path` and no path. `execute` renders the Makefile's block
from the plan's CURRENT `legs.spec_path`, and `MOUNT_REMEDIATION` and
`plan-leg-path-collides` both ask for a hand edit of it, so a path named
here went on naming the old mount in the split commit's message, the
pull request's own body, while the Makefile in that commit named the new
one (#522's review, F1).
"""
out = cross_leg_references(source, entries, spec_path, code_path)
if any(e.leg == "spec" and (e.path == "contracts/"
or e.path.startswith("contracts/"))
for e in entries):
out.append(
f"the root Makefile wires `CONTRACTS_DIR ?= $(CURDIR)/{spec_path}/"
"contracts` and exports it, so the code leg's tooling takes the "
"path from the environment instead of assuming it sits beside it")
"the root Makefile wires `CONTRACTS_DIR` to the `contracts/` of "
"the spec leg, where the plan's `legs.spec_path` mounts it, and "
"exports it, so the code leg's tooling takes the path from the "
"environment instead of assuming it sits beside it")
if any(e.path in ("CODEOWNERS", ".github/CODEOWNERS") for e in entries):
out.append(
"CODEOWNERS in the assembly root now names paths that live in the "
Expand Down Expand Up @@ -8706,8 +8747,10 @@ def _commit_the_split(plan: Plan, source: Source, assembly: Path,
step = "committing the split"
# The plan's submodule answers outlive the plan only here (#189).
# An empty record appends nothing, so a source with no submodule
# gets the manifest it always got.
append = {"Makefile": ADOPT_MAKEFILE_BLOCK}
# gets the manifest it always got. The Makefile's block names the
# spec leg where this plan mounts it (#522), and a default plan gets
# the block it always got.
append = {"Makefile": adopt_makefile_block(spec_path, code_path)}
record = submodule_record(plan.entries, source.tree(),
source.commit)
if record:
Expand Down
36 changes: 29 additions & 7 deletions scripts/shape_materialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -975,17 +975,34 @@ def copy_in_the_way(target: Path, actual: str) -> str | None:
return None


#: Appended to the assembly root's Makefile by `adopt-project.py` ONLY. A
#: scaffolded project's legs are empty, so nothing reads across them yet; an
#: ADOPTED project's code leg holds tooling that used to read `contracts/`
#: from beside it and now reads it from the other leg through the root.
ADOPT_MAKEFILE_BLOCK = """
def adopt_makefile_block(spec_path: str, code_path: str) -> str:
"""What `adopt-project.py` appends to the assembly root's Makefile, and
nothing else does. A scaffolded project's legs are empty, so nothing reads
across them yet; an ADOPTED project's code leg holds tooling that used to
read `contracts/` from beside it and now reads it from the other leg
through the root.

RENDERED WITH THE PLAN'S MOUNT PATHS (#522). It was one constant naming
`spec/contracts`, so a project adopted with `--spec-path legs/spec`
exported a `CONTRACTS_DIR` that does not exist, while `plan`'s own
follow-up said the Makefile wires `$(CURDIR)/legs/spec/contracts`. The
block is the adoption's own append, made once by `execute` and digested
with the copy it is appended to (`_materialize` pins the bytes as
written), so the per-project text is written here and nowhere by hand.
A default adoption, `spec` and `code`, gets the bytes it always got
(`ADOPT_MAKEFILE_BLOCK`).

Both paths are `execute`'s checked mount paths: canonical, and in
`checked_value`'s alphabet, which holds no character make reads as
anything but itself (no `$`, `#`, space or line break).
"""
return f"""
# --- adopted project: reading ACROSS the legs -------------------------------
# The spec leg owns `contracts/`; the code leg holds the tooling that reads
# them. Mounted here, `spec/contracts` is one relative path from `code/`, so
# them. Mounted here, `{spec_path}/contracts` is one relative path from `{code_path}/`, so
# the root exports it once and the code leg takes it from the environment
# rather than each script guessing at `../`.
CONTRACTS_DIR ?= $(CURDIR)/spec/contracts
CONTRACTS_DIR ?= $(CURDIR)/{spec_path}/contracts
export CONTRACTS_DIR

.PHONY: contracts-dir
Expand All @@ -994,6 +1011,11 @@ def copy_in_the_way(target: Path, actual: str) -> str | None:
"""


#: The block a DEFAULT adoption appends, the legs at `spec` and `code`:
#: byte for byte the block every adoption appended before #522.
ADOPT_MAKEFILE_BLOCK = adopt_makefile_block("spec", "code")


class CommandFailed(Exception):
def __init__(self, args, cwd, code, output):
super().__init__(" ".join(args))
Expand Down
Loading
Loading