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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ cd ~/src/my-fork && ~/src/ci-workflows/bin/start
It reads the repository's own workflows, lists the toolchains its CI
rows pull, and offers the plainest one first.

A repository whose CI does not use ci-workflows -- the
compiler-research website, say -- has no rows to read. Name it with
`--any-repo` instead, and pick the toolchain from every cell a dev
container can serve:

```bash
./bin/start --any-repo compiler-research/compiler-research.github.io
```

`./bin/start --list` prints the catalog without prompting
(`--list --repo <path>` does the same for a checkout). See
[Onboarding a contributor](docs/developer-guide.md#onboarding-a-contributor-binstart)
Expand Down
74 changes: 67 additions & 7 deletions bin/start
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ workflows are read instead: every setup-llvm /
setup-recipe call is evaluated against every matrix row (bin/cells.py),
and the cells a dev container can serve are offered, plainest first.

A repository whose CI does not use ci-workflows at all -- a website,
a paper, a tool's docs -- has nothing to read, so it gets its own
option, and the toolchain is picked from every cell a dev container
can serve:

./bin/start --any-repo compiler-research/compiler-research.github.io

Why a second entry point rather than a `bin/repro` flag: repro's
audience already knows what a matrix row is and why they want to
reproduce one. This one's audience has been handed a link by a
Expand Down Expand Up @@ -614,6 +621,8 @@ def start_checkout(repro, base: str, catalog: List[Dict[str, str]],
f"ci-workflows: no action or workflow of ours is referenced "
f"under .github/, so there is no cached toolchain to set "
f"up.")
print(f" bin/start --any-repo {checkout} opens it with a "
f"toolchain you pick.")
return None
usable, unusable = repo_cells(repro, checkout, catalog)
statuses = [cell_status(repro, base, coord, catalog)
Expand All @@ -635,6 +644,42 @@ def start_checkout(repro, base: str, catalog: List[Dict[str, str]],
return {"name": name, "repo": str(checkout)}, coord


def _any_repo_order(coord: Dict[str, str]) -> Tuple[int, ...]:
"""cells.plainest_first, then a purely numeric version before
one that is not ('23' before '23.1.0-rc2'): offered with no CI row
to go by, the newest final release is the safest default."""
return cells.plainest_first(coord, 0) + (
int(not coord["version"].isdigit()),)


def start_any_checkout(repro, base: str, catalog: List[Dict[str, str]],
checkout: Path
) -> Optional[Tuple[Dict[str, str], Dict[str, str]]]:
"""Pick a cell for a checkout whose CI need not use ci-workflows.

Same returns as start_checkout. The checkout's workflows are not
read: the user chooses from every published cell a dev container
can serve.
"""
name = checkout.name
offer = sorted((c for c in catalog if _devshell_servable(repro, c)),
key=_any_repo_order)
if not offer:
print(" no published cell can be opened in a dev container")
return None
print(f" {name} ({checkout}): its CI is not read; pick the toolchain "
f"the container should have.")
print()
for i, coord in enumerate(offer, start=1):
print(f" {i:>2} {coord_str(coord)}")
print(f" {cell_status(repro, base, coord, catalog)}")
print()
index = select(len(offer), "a toolchain", default=0)
if index is None:
return QUIT
return {"name": name, "repo": str(checkout)}, dict(offer[index])


# ------------------------------------------------------------------ main


Expand Down Expand Up @@ -685,13 +730,24 @@ def main(argv: Optional[List[str]] = None) -> int:
"--list", action="store_true",
help="print the catalog with cell status and exit (no prompts)",
)
parser.add_argument(
which = parser.add_mutually_exclusive_group()
which.add_argument(
"--repo", metavar="REPO",
help="use this repository instead of the catalog: a path, a clone "
"URL, or GitHub owner/repo (e.g. yourname/clad). Its "
"workflows must use ci-workflows",
)
which.add_argument(
"--any-repo", metavar="REPO",
help="like --repo, for a repository whose CI does not use "
"ci-workflows (e.g. compiler-research/"
"compiler-research.github.io): its workflows are not read, "
"and the toolchain is picked from every published cell",
)
args = parser.parse_args(argv)
if args.list and args.any_repo:
parser.error("--list does not take --any-repo: the cells it "
"offers are the catalog's, which --list prints")

projects = load_projects()
if not projects:
Expand Down Expand Up @@ -736,11 +792,13 @@ def main(argv: Optional[List[str]] = None) -> int:
base = cache_io.resolve_cache_base()
catalog = repro.published_cells()

if args.repo:
checkout = custom_checkout(args.repo)
if args.repo or args.any_repo:
checkout = custom_checkout(args.repo or args.any_repo)
if checkout is None:
return 1
return _launch_checkout(repro, base, catalog, checkout)
return _launch_checkout(
repro, base, catalog, checkout,
pick=start_any_checkout if args.any_repo else start_checkout)

detected = detect_project(repro, projects)
here = _git_toplevel(Path.cwd())
Expand Down Expand Up @@ -801,9 +859,11 @@ def main(argv: Optional[List[str]] = None) -> int:


def _launch_checkout(repro, base: str, catalog: List[Dict[str, str]],
checkout: Path) -> int:
"""--repo / `o` / an uncatalogued cwd: scan, pick a cell, launch."""
picked = start_checkout(repro, base, catalog, checkout)
checkout: Path, pick=start_checkout) -> int:
"""--repo / --any-repo / `o` / an uncatalogued cwd: pick a cell
(start_checkout scans the workflows; start_any_checkout does not),
launch."""
picked = pick(repro, base, catalog, checkout)
if picked is None:
return 1
if picked is QUIT:
Expand Down
66 changes: 66 additions & 0 deletions bin/test_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,72 @@ def test_clad_is_in_the_catalog(self):
cell, start.cells.load_catalog()))


class AnyRepoOptionTest(unittest.TestCase):
"""--any-repo: a repository whose CI does not use ci-workflows, the
compiler-research website as the example."""

def test_any_servable_cell_is_offered_and_its_ci_is_not_read(self):
with mock.patch("builtins.input", side_effect=[""]), \
mock.patch.object(start, "_asset_size", return_value=None), \
mock.patch.object(start.cells, "scan",
side_effect=AssertionError("no scan")), \
redirect_stdout(io.StringIO()) as out:
got = start.start_any_checkout(_fake_repro(), "file:///x", _CELLS,
_checkout(ci=None))
project, coord = got
self.assertEqual(coord, _CELLS[0]) # llvm-release before asan
self.assertEqual(project["name"], "clad")
text = out.getvalue()
self.assertIn("llvm-asan/23/ubuntu-24.04/x86_64", text)
self.assertNotIn("macos-26", text) # no dev container for it
self.assertEqual(text.count("not published yet"), 2)

def test_a_release_candidate_comes_after_its_release(self):
rc = dict(_CELLS[0], version="23.1.0-rc2")
final = dict(_CELLS[0], version="23")
with mock.patch("builtins.input", side_effect=[""]), \
mock.patch.object(start, "_asset_size", return_value=None), \
redirect_stdout(io.StringIO()):
_project, coord = start.start_any_checkout(
_fake_repro(), "file:///x", [rc, final], _checkout(ci=None))
self.assertEqual(coord, final)

def test_quit_at_the_toolchain_prompt(self):
with mock.patch("builtins.input", side_effect=["q"]), \
redirect_stdout(io.StringIO()):
got = start.start_any_checkout(_fake_repro(), "file:///x",
_CELLS, _checkout(ci=None))
self.assertIs(got, start.QUIT)

def test_repo_refusal_points_at_any_repo(self):
with redirect_stdout(io.StringIO()) as out:
start.start_checkout(_fake_repro(), "file:///x", _CELLS,
_checkout(ci=None))
self.assertIn("--any-repo", out.getvalue())

def test_main_launches_the_picked_cell(self):
co = _checkout(ci=None)
repro = _fake_repro()
repro.published_cells = mock.Mock(return_value=_CELLS)
with mock.patch.object(start, "preflight", return_value=True), \
mock.patch.object(start, "_load_repro", return_value=repro), \
mock.patch.object(start, "_asset_size", return_value=None), \
mock.patch("builtins.input", side_effect=["2"]), \
redirect_stdout(io.StringIO()):
self.assertEqual(start.main(["--any-repo", str(co)]), 0)
ns = repro.cmd_devshell.call_args[0][0]
self.assertEqual(ns.matrix,
["name:llvm-asan/23/ubuntu-24.04/x86_64"])
self.assertEqual(ns.devshell_patches_out, str(co))

def test_repo_and_any_repo_are_exclusive(self):
for argv in (["--repo", "a/b", "--any-repo", "c/d"],
["--list", "--any-repo", "c/d"]):
with self.assertRaises(SystemExit), \
redirect_stderr(io.StringIO()):
start.main(argv)


class InterruptTest(unittest.TestCase):
def test_ctrl_c_at_the_menu_exits_quietly(self):
with mock.patch.object(start, "preflight", return_value=True), \
Expand Down
Loading