Skip to content
40 changes: 38 additions & 2 deletions scripts/bump-leg.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@
says so and moves the commit, exactly as the real run does (#534). A
tree, a blob or a tag of either is refused `bump-leg-target-not-a-commit`
by name, in both runs.
* a manifest whose `legs:` is not a list, or holds an entry that is not a
mapping: `bump-leg-manifest-legs`, naming each in the words of the
project's own `validate-manifest.py`, before anything is fetched and
under `--dry-run` as well (#832). That validator refuses either, so a
bump over one could never land.
* a commit the leg's REMOTE does not have. A pin nobody else can fetch is a
root nobody else can bootstrap.
* a dirty root. The commit is made with explicit pathspecs, so unrelated
Expand Down Expand Up @@ -205,8 +210,39 @@ def open(cls, root_spec) -> "Project":

@property
def legs(self) -> list[dict]:
return [row for row in (self.data.get("legs") or [])
if isinstance(row, dict)]
"""The manifest's legs, in its own order.

A `legs:` THAT IS NOT A LIST, OR AN ENTRY OF IT THAT IS NOT A MAPPING,
IS REFUSED `bump-leg-manifest-legs` (#832), naming each in
`validate-manifest.py`'s words for it (#714), before anything is
fetched or written and under `--dry-run` as well. Iterated as `legs
or []`, `legs: 5` ended the run in a `TypeError`, and a scalar
`openXdox`, a mapping or `[5]` was refused `bump-leg-unknown-leg`,
"it declares" with nothing after it, and the remedy of a `--leg`
that was not wrong. A list holding a leg beside such an entry was
previewed, exit 0, and the real run refused `bump-leg-validators-red`
on that validator's `manifest-leg` finding: a bump that cannot land
is refused before the dry run says it would. Absent or null declares
no leg, which `leg` refuses `bump-leg-unknown-leg` as it did.
"""
declared = self.data.get("legs")
if declared is None:
return []
if not isinstance(declared, list):
said = [f"manifest-legs: legs is {declared!r}, expected a list of "
"`role:`, `repository:` and `path:` mappings"]
else:
said = [f"manifest-leg: a leg is not a mapping: {row!r}"
for row in declared if not isinstance(row, dict)]
if said:
raise Refusal(
"bump-leg-manifest-legs", f"{self.path}: " + "; ".join(said),
"Remediation: each entry of `legs:` is one leg, a mapping "
"with `role:`, `repository:` and `path:`. The project's own "
"`scripts/validate-manifest.py` names this same fault; a leg "
"is not pinned around a manifest defect, so that comes "
"first, then this command again.")
return declared

def leg(self, role: str) -> dict:
# `assembly` IS declared, at path `.`, and is refused BEFORE the
Expand Down
92 changes: 79 additions & 13 deletions shape-doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1368,10 +1368,37 @@ def _read(path: Path):
return None

def legs(self) -> list[dict]:
"""The non-assembly legs `project.yaml` declares, in its own order."""
return [leg for leg in ((self.manifest or {}).get("legs") or [])
"""The non-assembly legs `project.yaml` declares, in its own order.

A `legs:` THAT IS NOT A LIST DECLARES NO LEG THIS CAN NAME (#832), as
`validate-manifest.py` reads it (#714), and an entry that is not a
mapping is no leg; `legs_unread` says each. Iterated as `legs or []`,
`legs: 5` ended the whole report in a `TypeError`, in `declared_pins`
under the `manifest kinds` row, and a scalar `openXdox` or a mapping
was walked one character, or one key, at a time, so the `legs` row
called the project one with no legs.
"""
declared = (self.manifest or {}).get("legs")
if not isinstance(declared, list):
return []
return [leg for leg in declared
if isinstance(leg, dict) and leg.get("role") != "assembly"]

def legs_unread(self) -> list[str]:
"""What in `legs:` names no leg `legs` can read, each as
`validate-manifest.py`'s finding, in its words (#714, #832): a
`legs:` that is not a list, or each entry of one that is not a
mapping. Absent or null is neither: it declares no leg, and
`validate-manifest.py` says `legs is missing or empty` of it."""
declared = (self.manifest or {}).get("legs")
if declared is None:
return []
if not isinstance(declared, list):
return [f"manifest-legs: legs is {declared!r}, expected a list of "
"`role:`, `repository:` and `path:` mappings"]
return [f"manifest-leg: a leg is not a mapping: {leg!r}"
for leg in declared if not isinstance(leg, dict)]

def members(self) -> list[dict]:
return [row for row in ((self.manifest or {}).get("members") or [])
if isinstance(row, dict)]
Expand Down Expand Up @@ -2357,6 +2384,36 @@ def waiting_row(ctx: Context, check_id: str, label: str, needs: str,
{**(detail or {}), "waits_on": owner})


def legs_unread_row(ctx: Context, check_id: str, label: str, needs: str,
detail: dict | None = None) -> Row | None:
"""The `n/a` a row that reads the LEGS gives where `legs:` names none it
can read (#832), or None: where it names a leg, and where it names none
at all (absent, null or `[]`), which the row's own sentence is about.

A `legs:` that is not a list, or a list in which no leg but the assembly
leg is a mapping and some entry is not one, is not a project with no
legs, which is what these rows said of `openXdox`, `{a: b}` and `[5]`
("a one-repository project has none"): it is a manifest nobody can read
legs out of. ONE FAULT, ONE FINDING, as `waiting_row` reasons: the
project's own `validate-manifest.py` says the fault, and the `manifest`
row runs it, so this row is `n/a`, says the same sentence
(`Context.legs_unread`) and names that row in `waits_on`. A list that
holds a leg as well as an entry that is not one is checked leg by leg,
as `validate-pins.py` checks the rest past such an entry, and the entry
is left to the `manifest` row.
"""
unread = ctx.legs_unread()
if not unread or ctx.legs():
return None
what = ("holds an entry that is not a mapping"
if isinstance(ctx.manifest.get("legs"), list) else "is not a list")
return Row(check_id, label, NA,
f"{ctx.manifest_path.name}'s `legs:` {what} "
f"({'; '.join(unread)}), so {needs}; the `manifest` row's "
"command names the fault", None,
{**(detail or {}), "waits_on": "manifest"})


# ---------------------------------------------------------------------------
# The checks
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -6210,7 +6267,10 @@ def check_legs(ctx: Context) -> Row:

THE LEGS ARE THE MANIFEST'S, and a manifest that does not load declares
none that this row can name -- which is not "declares no non-assembly
leg", the sentence below, about a project that has none (#205).
leg", the sentence below, about a project that has none (#205). Nor is
a `legs:` that is not a list, or one holding only entries that are not
mappings: `legs_unread_row` says which, in `validate-manifest.py`'s
words, and waits on the `manifest` row (#832).

THE NEXT IS CHOSEN BY WHAT IS WRONG, AND IT CURES THE FIRST KIND (#276).
`make bootstrap` was named for all of it and does not answer it: it
Expand Down Expand Up @@ -6278,9 +6338,10 @@ def check_legs(ctx: Context) -> Row:
an unborn HEAD or a leg with no url gets the same sentence it gets with
no line at all, which ends by saying to run the doctor again.
"""
waiting = waiting_row(ctx, "legs", "legs",
"the legs it declares are not known and none is "
"checked against its pin", {"legs": []})
needs = ("the legs it declares are not known and none is checked against "
"its pin")
waiting = (waiting_row(ctx, "legs", "legs", needs, {"legs": []})
or legs_unread_row(ctx, "legs", "legs", needs, {"legs": []}))
if waiting is not None:
return waiting
legs = ctx.legs()
Expand Down Expand Up @@ -6634,8 +6695,11 @@ def check_leg_shape_files(ctx: Context) -> Row:
compare every leg, say what was found, and offer a copy for the files a
copy would fix.
"""
waiting = waiting_row(ctx, "leg-shape-files", LABEL_LEG_SHAPE_FILES,
"there are no legs to compare", {"legs": []})
needs = "there are no legs to compare"
waiting = (waiting_row(ctx, "leg-shape-files", LABEL_LEG_SHAPE_FILES,
needs, {"legs": []})
or legs_unread_row(ctx, "leg-shape-files",
LABEL_LEG_SHAPE_FILES, needs, {"legs": []}))
if waiting is not None:
return waiting
legs = ctx.legs()
Expand Down Expand Up @@ -7810,11 +7874,13 @@ def check_placement(ctx: Context) -> Row:
"a family holder has no legs of its own: each member is an "
"assembly root and answers this in its own report")
# Before the sentence below, which is about a project that declares no
# leg, not one whose manifest nobody could read (#205). And `n/a` is what
# makes `--placement-plan` refuse by name and write nothing: there is no
# leg to write a plan about until the manifest loads.
waiting = waiting_row(ctx, "placement", "placement",
"there is no leg to classify a path in")
# leg, not one whose manifest nobody could read (#205), nor one whose
# `legs:` names no leg that can be read (#832). And `n/a` is what makes
# `--placement-plan` refuse by name and write nothing: there is no leg to
# write a plan about until the manifest loads and names one.
needs = "there is no leg to classify a path in"
waiting = (waiting_row(ctx, "placement", "placement", needs)
or legs_unread_row(ctx, "placement", "placement", needs))
if waiting is not None:
return placement_row(NA, waiting.reason, None, waiting.detail)
legs = ctx.legs()
Expand Down
49 changes: 42 additions & 7 deletions templates/assembly-root/scripts/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -1251,18 +1251,53 @@ def _load_root_and_manifest(args) -> tuple[Path, dict | None]:
return root, manifest


def _legs_of(manifest: dict | None) -> tuple[list[dict], list[str]]:
"""`(the legs, what names no leg)`: each entry of `project.yaml`'s
`legs:` that is a mapping, and for what is not, `validate-manifest.py`'s
finding in its words (#714, #832).

Iterated as `legs or []`, `legs: 5` ended `make bootstrap` in a
`TypeError` before step (a) printed a line, and a scalar `openXdox` or a
mapping was walked one character, or one key, at a time, so step (a)
said "no submodule legs declared", as of a one-repository project. A
`legs:` that is not a list names no leg; a list's entry that is not a
mapping is no leg, and the mappings beside it are still placed, as
`validate-pins.py` checks the rest past such an entry. Absent or null
declares no leg and says nothing here: step (b)'s manifest validator
says `legs is missing or empty`.
"""
declared = (manifest or {}).get("legs")
if declared is None:
return [], []
if not isinstance(declared, list):
return [], [f"manifest-legs: legs is {declared!r}, expected a list of "
"`role:`, `repository:` and `path:` mappings; no leg is "
"placed, and the validators below refuse until `legs:` "
"is a list"]
return ([leg for leg in declared if isinstance(leg, dict)],
[f"manifest-leg: a leg is not a mapping: {leg!r}; it is not "
"placed, and the validators below refuse until it is a mapping"
for leg in declared if not isinstance(leg, dict)])


def _checkout_all_tracking_branches(root: Path, sub_legs: list[dict],
manifest: dict | None,
branch_arg: str | None) -> None:
branch_arg: str | None,
unread: list[str] = ()) -> None:
"""Step (a): place every submodule leg on its tracking branch.

Split from `main` for #132. Git that cannot be started is
`git-not-runnable`, naming the leg it met it at (#694): one fact about
the machine, so no leg after it is tried.
the machine, so no leg after it is tried. `unread` is what `_legs_of`
found naming no leg, each said first, and a step with nothing else to
place is not a one-repository project's no-op (#832).
"""
for line in unread:
print(f" {line}")
if not sub_legs:
print(" no submodule legs declared. A one-repository project runs "
"the same command and this step is a no-op.")
if not unread:
print(" no submodule legs declared. A one-repository project "
"runs the same command and this step is a no-op.")
return
branch = branch_arg or (manifest or {}).get("tracking_branch") or "main"
for leg in sub_legs:
Expand Down Expand Up @@ -1317,16 +1352,16 @@ def main(argv: list[str] | None = None) -> int:
print(str(exc), file=sys.stderr)
return 2

legs = [leg for leg in ((manifest or {}).get("legs") or [])
if isinstance(leg, dict)]
legs, unread = _legs_of(manifest)
sub_legs = [leg for leg in legs if leg.get("role") != "assembly"]

name = (manifest or {}).get("name") or root.name
print(f"bootstrap: {spelled(name)} ({spelled(root)})")

try:
print("\n(a) legs on tracking branches, pins untouched")
_checkout_all_tracking_branches(root, sub_legs, manifest, args.branch)
_checkout_all_tracking_branches(root, sub_legs, manifest, args.branch,
unread)

print("\n(b) neutral validators")
failed = _run_neutral_validators(root, manifest, args.skip_validators)
Expand Down
Loading
Loading