Skip to content

Commit c870c14

Browse files
redsun82Copilot
andcommitted
Just: add the shared machinery behind the common verbs
Introduces a small set of verbs (`test`, `build`, `generate`, `format`, `lint`) that work the same from anywhere in the tree, so that contributors do not have to remember a different incantation per language. Running a verb from the root forwards it to whichever justfile actually implements it for the given paths; running it in a language directory uses that language's definition directly. Everything language-specific stays in the per-language justfiles added next; this commit only provides the vocabulary they share: - `misc/just/forward.just` and `forward_command.py` resolve a verb plus a set of paths to the justfiles that implement it, grouping paths per justfile. - `misc/just/lib.just` exposes `_codeql_test`, `_language_tests` and `_integration_test` for the per-language justfiles to build on. - `codeql_test_run.py` turns test flags into a `codeql test run` invocation, resolving `RAM_PER_THREAD`/`CPUS` from arguments, environment, then platform defaults. - `misc/just/defs.just` holds the settings and generic helpers, including the internal-checkout detection that lets the same justfiles work in both repos. Arguments are passed around as just lists (`set lists`), so values containing spaces survive intact rather than being re-split by the helpers. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent def1184 commit c870c14

13 files changed

Lines changed: 539 additions & 0 deletions

justfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# see misc/just/README.md for an overview
2+
3+
import 'lib.just'
4+
import 'misc/just/forward.just'

lib.just

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import "misc/just/lib.just"

misc/just/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
This directory contains an infrastructure for [`just`](https://github.com/casey/just)
2+
recipes that can be used throughout this and the internal repository. In particular we
3+
have common verbs (`build`, `test`, `format`, `lint`, `generate`) that individual parts
4+
of the project can implement, and some common functionality that can be used to that
5+
effect.
6+
7+
# Forwarding
8+
9+
The core of the functionality is given by forwarding. The idea is that:
10+
11+
- if you are in the directory where a verb is implemented, you will get that as per
12+
standard `just` behaviour (possibly using fallback).
13+
- if on the other hand you are above it, and you run something like
14+
`just test ql/rust/ql/test/{a,b}`, then a forwarder script finds a common justfile
15+
implementing the verb for all the positional arguments passed there, and then retries
16+
calling `just test` from there. So if `test` is implemented beneath that (in that case,
17+
it is in `rust/ql/test`), it uses that recipe.
18+
- even if there isn't a recipe that is common to all the positional arguments, the
19+
forwarder will still group the arguments in batches using the same recipe. So
20+
`just build ql/rust ql/java`, or
21+
`just test ql/rust/ql/test/some/language/test ql/rust/ql/integration-test/some/integration/test`
22+
will also work, with corresponding recipes run sequentially.
23+
24+
Another point is how launching QL tests can be tweaked:
25+
26+
- by default, the corresponding CLI is built from the internal repo (nothing is done if
27+
working in `codeql` standalone), and no additional database or consistency checks are
28+
made
29+
- `--codeql=built` can be passed to skip the build step (if no changes were made to the
30+
CLI/extractors). This is consistent with the same pytest option
31+
- you can add the additional checks that CI does with `--all-checks` or the `+`
32+
abbreviation. These additional checks are configured in justfiles per language, and
33+
correspond to all the additional checks that CI adds (but that a dev might not want to
34+
run by default).
35+
36+
Test arguments are passed around as `just` lists (`set lists`), so they reach the
37+
underlying runner already split and arguments containing spaces survive intact.
38+
39+
One caveat: when running different recipes for the same verb, non-positional arguments
40+
need to be supported by all recipes involved. For example, this will work ok for
41+
`--learn` or `--codeql` options in language and integration tests.

misc/just/build.just

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Helper build recipes
2+
3+
import "defs.just"
4+
5+
# Build the given language-specific CLI distribution
6+
_build_dist LANGUAGE: _require_semmle_code (_maybe_build_dist LANGUAGE)
7+
8+
# Build the language-specific distribution if we are in an internal repository checkout
9+
# Otherwise, do nothing
10+
[no-exit-message]
11+
_maybe_build_dist LANGUAGE: (_if_in_semmle_code (f'cd "$SEMMLE_CODE"; MSYS2_ARG_CONV_EXCL="*" tools/bazel test //language-packs:intree-{{ LANGUAGE }}-as-test --test_output=all') '# using codeql from PATH, if any')
12+
13+
# Call bazel. Uses our official bazel wrapper if we are in an internal repository checkout
14+
[no-cd]
15+
[no-exit-message]
16+
_bazel *ARGS: (_if_in_semmle_code 'cd "$SEMMLE_CODE"; MSYS2_ARG_CONV_EXCL="*" tools/bazel' 'bazel' ARGS)
17+
18+
# Call sembuild (requires an internal repository checkout)
19+
[no-cd]
20+
[no-exit-message]
21+
_sembuild *ARGS: (_run_in_semmle_code (['./build'] ++ ARGS))

misc/just/codeql_test_run.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#!/usr/bin/env python3
2+
"""Run CodeQL tests with appropriate configuration.
3+
4+
Called from just recipes as:
5+
python3 codeql_test_run.py LANGUAGE [ARG...]
6+
7+
Arguments are already split by `just` (see `set lists`), so each one is taken verbatim.
8+
`--all-checks=FLAG` contributes FLAG to the set of extra checks that `--all-checks` (or
9+
its `+` abbreviation) turns on.
10+
"""
11+
12+
import os
13+
import re
14+
import subprocess
15+
import sys
16+
from pathlib import Path
17+
18+
JUST = os.environ.get("JUST_EXECUTABLE", "just")
19+
ERROR = os.environ.get("JUST_ERROR", "error: ")
20+
CMD_BEGIN = os.environ.get("CMD_BEGIN", "")
21+
CMD_END = os.environ.get("CMD_END", "")
22+
SEMMLE_CODE = os.environ.get("SEMMLE_CODE")
23+
24+
ALL_CHECKS_PREFIX = "--all-checks="
25+
ENV_RE = re.compile(r"^[A-Z_][A-Z_0-9]*=.*$")
26+
27+
28+
def invoke(invocation, *, cwd=None, log_prefix=""):
29+
prefix = f"{log_prefix} " if log_prefix else ""
30+
print(f"{CMD_BEGIN}{prefix}{' '.join(invocation)}{CMD_END}")
31+
try:
32+
subprocess.run(invocation, check=True, cwd=cwd)
33+
except subprocess.CalledProcessError as e:
34+
return e.returncode
35+
return 0
36+
37+
38+
def error(message):
39+
print(f"{ERROR}{message}", file=sys.stderr)
40+
41+
42+
def parse_args(args, argv):
43+
"""Sort arguments into tests, flags and environment assignments."""
44+
for arg in argv:
45+
if not arg:
46+
# an empty argument can come from a caller interpolating an unset variable
47+
continue
48+
if arg.startswith(ALL_CHECKS_PREFIX):
49+
args["all_checks"].append(arg[len(ALL_CHECKS_PREFIX) :])
50+
elif arg.startswith("--codeql="):
51+
args["codeql"] = arg.split("=", 1)[1]
52+
elif arg in ("+", "--all-checks"):
53+
args["all"] = True
54+
elif arg.startswith("-"):
55+
args["flags"].append(arg)
56+
elif ENV_RE.match(arg):
57+
args["env"].append(arg)
58+
else:
59+
args["tests"].append(arg)
60+
61+
62+
def env_value(args, name, default):
63+
"""Resolve a setting from test arguments, then the environment, then a default."""
64+
for assignment in reversed(args["env"]):
65+
key, _, value = assignment.partition("=")
66+
if key == name and value:
67+
return value
68+
return os.environ.get(name) or default
69+
70+
71+
def main():
72+
argv = sys.argv[1:]
73+
if not argv:
74+
error("Usage: codeql_test_run.py LANGUAGE [ARG...]")
75+
return 1
76+
77+
language, *rest = argv
78+
79+
args = {
80+
"tests": [],
81+
"flags": [],
82+
"env": [],
83+
"all_checks": [],
84+
"codeql": "build" if SEMMLE_CODE else "host",
85+
"all": False,
86+
}
87+
parse_args(args, rest)
88+
if args["all"]:
89+
parse_args(args, args["all_checks"])
90+
91+
if not SEMMLE_CODE and args["codeql"] in ("build", "built"):
92+
error(
93+
"Using `--codeql=build` or `--codeql=built` requires working "
94+
"with the internal repository"
95+
)
96+
return 1
97+
98+
if not args["tests"]:
99+
args["tests"].append(".")
100+
101+
# Resolve these only once all arguments are known, so that a `RAM_PER_THREAD=` test
102+
# argument can lower the default on memory-heavy suites.
103+
default_ram = 3000 if sys.platform == "linux" else 2048
104+
ram_per_thread = int(env_value(args, "RAM_PER_THREAD", default_ram))
105+
cpus = int(env_value(args, "CPUS", os.cpu_count() or 1))
106+
args["flags"][:0] = [f"--ram={ram_per_thread * cpus}", f"-j{cpus}"]
107+
108+
if args["codeql"] == "build":
109+
if invoke([JUST, language, "build"], cwd=SEMMLE_CODE) != 0:
110+
return 1
111+
112+
if args["codeql"] != "host":
113+
# Disable the default implicit config file, but keep an explicit one.
114+
# Same behavior wrt --codeql as the integration test runner.
115+
os.environ.setdefault("CODEQL_CONFIG_FILE", ".")
116+
117+
for env_var in args["env"]:
118+
key, _, value = env_var.partition("=")
119+
if not key:
120+
error(f"Invalid environment variable assignment: {env_var}")
121+
return 1
122+
os.environ[key] = value
123+
124+
# Resolve codeql executable
125+
if args["codeql"] in ("built", "build"):
126+
codeql = Path(SEMMLE_CODE, "target", "intree", f"codeql-{language}", "codeql")
127+
elif args["codeql"] == "host":
128+
codeql = Path("codeql")
129+
else:
130+
codeql = Path(args["codeql"])
131+
132+
if codeql.is_dir():
133+
codeql = codeql / "codeql"
134+
135+
# On Windows, prefer codeql.exe over the Unix shell wrapper
136+
if sys.platform == "win32" and codeql.suffix != ".exe":
137+
exe = codeql.with_suffix(".exe")
138+
if exe.exists():
139+
codeql = exe
140+
141+
if args["codeql"] != "host" and not codeql.exists():
142+
error(f"CodeQL executable not found: {codeql}")
143+
return 1
144+
145+
return invoke(
146+
[str(codeql), "test", "run", *args["flags"], "--", *args["tests"]],
147+
log_prefix=" ".join(args["env"]),
148+
)
149+
150+
151+
if __name__ == "__main__":
152+
try:
153+
sys.exit(main())
154+
except KeyboardInterrupt:
155+
sys.exit(128 + 2)

misc/just/defs.just

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import? '../../../semmle-code.just' # internal repo just file, if present
2+
import 'semmle-code-stub.just'
3+
4+
# `set lists` is what lets recipes forward argument lists without encoding them as
5+
# whitespace separated strings. It is still unstable as of just 1.58.
6+
set unstable
7+
set lists
8+
set fallback
9+
set allow-duplicate-recipes
10+
set allow-duplicate-variables
11+
12+
export PATH_SEP := if os() == "windows" { ";" } else { ":" }
13+
export JUST_EXECUTABLE := just_executable()
14+
15+
error := f'{{ style("error") }}error{{ NORMAL }}: '
16+
cmd_sep := "\n#--------------------------------------------------------\n"
17+
export CMD_BEGIN := style("command") + cmd_sep
18+
export CMD_END := cmd_sep + NORMAL
19+
export JUST_ERROR := error
20+
21+
py := "python3"
22+
23+
default_db_checks := ['--check-databases', '--check-diff-informed', '--fail-on-trap-errors']
24+
25+
[no-exit-message]
26+
@_require_semmle_code:
27+
{{ if SEMMLE_CODE == "" { f'''
28+
echo "{error} running this recipe requires doing so from an internal repository checkout" >&2
29+
exit 1
30+
''' } else { "" } }}
31+
32+
[no-cd]
33+
_run +ARGS:
34+
{{ cmd_sep }}{{ ARGS }}{{ cmd_sep }}
35+
36+
[no-cd]
37+
_run_in DIR +ARGS:
38+
{{ cmd_sep }}cd "{{ DIR }}"; {{ ARGS }}{{ cmd_sep }}
39+
40+
[no-cd]
41+
_run_in_semmle_code +ARGS: _require_semmle_code (_run_in "$SEMMLE_CODE" ARGS)
42+
43+
[no-cd]
44+
[no-exit-message]
45+
[positional-arguments]
46+
@_just +ARGS:
47+
echo "-> just $@"
48+
"{{ JUST_EXECUTABLE }}" "$@"
49+
50+
[no-cd]
51+
[positional-arguments]
52+
@_if_not_on_ci_just +ARGS:
53+
if [ "${GITHUB_ACTIONS:-}" != "true" ]; then \
54+
echo "-> just $@"; \
55+
"$JUST_EXECUTABLE" "$@"; \
56+
fi
57+
58+
[no-cd]
59+
[no-exit-message]
60+
_if_in_semmle_code THEN ELSE *ARGS:
61+
{{ cmd_sep }}{{ if SEMMLE_CODE != "" { THEN } else { ELSE } }} {{ ARGS }}{{ cmd_sep }}

misc/just/format.just

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import "build.just"
2+
3+
[no-cd]
4+
[no-exit-message]
5+
_format_ql +ARGS: (_maybe_build_dist "nolang") (_if_in_semmle_code '"$SEMMLE_CODE/target/intree/codeql-nolang/codeql"' 'codeql' (f"query format --in-place -v $(find {{ ARGS }} -type f -name '*.ql' -or -name '*.qll')"))
6+
7+
[no-cd]
8+
[no-exit-message]
9+
_format_py *ARGS=".": (_if_in_semmle_code "uv run black" "black" ARGS)
10+
11+
[no-cd]
12+
[no-exit-message]
13+
_format_cpp *ARGS=".": (_if_in_semmle_code "uv run clang-format" "clang-format" (f"-i --verbose $(find {{ ARGS }} -type f -name '*.h' -or -name '*.cpp')"))

misc/just/forward.just

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Common verbs
2+
# See README.md in this directory for an overview.
3+
4+
import "lib.just"
5+
6+
# Verbs are recipe names, so each one needs its own recipe. They all delegate to the
7+
# same forwarder, which decides where the verb is actually implemented.
8+
9+
[no-cd]
10+
[no-exit-message]
11+
[positional-arguments]
12+
@_forward VERB *ARGS:
13+
{{ py }} "{{ source_dir() }}/forward_command.py" "$@"
14+
15+
alias t := test
16+
alias b := build
17+
alias g := generate
18+
alias gen := generate
19+
alias f := format
20+
alias l := lint
21+
22+
test *ARGS: (_forward "test" ARGS)
23+
24+
build *ARGS: (_forward "build" ARGS)
25+
26+
generate *ARGS: (_forward "generate" ARGS)
27+
28+
lint *ARGS: (_forward "lint" ARGS)
29+
30+
format *ARGS: (_forward "format" ARGS)

0 commit comments

Comments
 (0)