Skip to content

Commit c64d36d

Browse files
r-barnesmeta-codesync[bot]
authored andcommitted
Render generate-github-actions output via Jinja2
Summary: `GenerateGitHubActionsCmd.write_job_for_platform` previously emitted GitHub Actions workflow YAML through ~120 inline `out.write()` calls interleaved with platform conditionals — a wall of string-building that made every CI-shape change a delicate edit. This diff replaces that with a single Jinja2 template plus a context-builder method, while preserving byte-identical output for every existing invocation. The template lives at `getdeps/templates/workflow.yml.j2`. To keep GitHub Actions `${{ ... }}` expressions readable inside the template, the Jinja environment uses non-default delimiters: `<< >>` for variables, `<% %>` for blocks, `<# #>` for comments. `keep_trailing_newline=True` is set so the template's final newline is preserved verbatim. The literal string `generated` in the file header is constructed at render time as `<<"@" + "generated">>` so the template itself isn't flagged as a generated file. `jinja2` is imported lazily inside `_render_workflow` so OSS GHA runners executing `getdeps.py build`/`test`/`install-system-deps` are not forced to install it; only callers of `generate-github-actions` need it on their system. The refactor also splits `getdeps.py` into a thin OSS-facing shim and a `getdeps/cli.py` module that holds all the command classes, `parse_args`, and `main`. Supporting pieces moved with it: `ProjectCmdBase`, `UsageError`, and `BUILD_TYPE_ARG` to `getdeps/cmd_base.py`; `GenerateGitHubActionsCmd` plus its rendering helpers (`_render_workflow`, `_build_render_context`, `_parse_per_package_defines`) to `getdeps/workflow_generator.py`. The shim's only job is `sys.path.insert` for OSS users running `python3 build/fbcode_builder/getdeps.py …` directly. Inside Buck, a new `python_binary` target `//opensource/fbcode_builder:getdeps` invokes `getdeps.cli:main`, brings the third-party deps along, and resolves the long-standing namespace collision between `getdeps.py` (script) and `getdeps/` (package). `update-all-github-actions.sh` now invokes via `buck run` so Meta engineers no longer need jinja2 installed in their system python. The new `_build_render_context` method does all data preparation (manifest walk, dep ordering, command-string composition, env-var assembly) and returns a flat `dict` consumed by the template. CLI-string assembly (`build_type_arg`, `cmake_arg_for(...)`, `getdepscmd ...`) stays in Python — composing these in Jinja would be worse than what we have today. Several small simplifications fall out of the move: the per-OS `(artifacts, runs_on, py3)` derivation moves into a `_resolve_platform(args, build_opts)` helper at module scope; the `_PLATFORMS` list becomes a module-level constant; the five repeated `manifest.get("github.actions", ..., ctx=manifest_ctx)` lookups go through a local `gh(key)` closure; and the `tests_arg`/`job_file_prefix`/`job_name` "default-then-override" blocks collapse to single `or`/ternary expressions. With those out, `write_job_for_platform` no longer needs its `noqa: C901`. Output equivalence is verified two ways. (1) `getdeps/test/workflow_generator_test.py` is a golden test that drives the generator in-process for four representative scenarios (xxhash all-OSes, folly shared-libs linux, openr no-system-packages run-on-all-branches, rebalancer with cmake overrides) and asserts each emitted YAML file matches a checked-in fixture. When the fixture and actual output drift, the assertion failure prints how to regenerate. The test accepts a `--update-fixtures` flag (`buck run //opensource/fbcode_builder/getdeps/test:test -- --update-fixtures`) which rewrites the per-scenario fixture files from current output and skips assertions; the resulting `sl status` diff is the change record. (2) Running every invocation in `getdeps/facebook/update-all-github-actions.sh` against pre- and post-refactor builds and diffing produces zero differences across all 44 generated `.yml` files. This change does not touch the CLI flag surface, schema, or output files of `update-all-github-actions.sh`. It is a pure refactor. Reviewed By: bigfootjon Differential Revision: D104495377 fbshipit-source-id: c7eb6a118c94d41fdc4525235f2319772f259812
1 parent 169332c commit c64d36d

14 files changed

Lines changed: 4387 additions & 1753 deletions

File tree

build/fbcode_builder/getdeps.py

100755100644
Lines changed: 9 additions & 1751 deletions
Large diffs are not rendered by default.

build/fbcode_builder/getdeps/cli.py

Lines changed: 996 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
#
3+
# This source code is licensed under the MIT license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
import os
7+
from typing import Any
8+
9+
from .buildopts import setup_build_options
10+
from .load import ManifestLoader
11+
from .subcmd import SubCmd
12+
13+
14+
class UsageError(Exception):
15+
pass
16+
17+
18+
# Shared argument definition for --build-type used by multiple commands
19+
BUILD_TYPE_ARG: dict[str, Any] = {
20+
"help": "Set the build type explicitly: Debug (unoptimized, debug symbols), RelWithDebInfo (optimized with debug symbols, default), MinSizeRel (size-optimized, no debug), or Release (optimized, no debug).",
21+
"choices": ["Debug", "Release", "RelWithDebInfo", "MinSizeRel"],
22+
"action": "store",
23+
"default": "RelWithDebInfo",
24+
}
25+
26+
27+
class ProjectCmdBase(SubCmd):
28+
def run(self, args):
29+
opts = setup_build_options(args)
30+
31+
if args.current_project is not None:
32+
opts.repo_project = args.current_project
33+
if args.project is None:
34+
if opts.repo_project is None:
35+
raise UsageError(
36+
"no project name specified, and no .projectid file found"
37+
)
38+
if opts.repo_project == "fbsource":
39+
# The fbsource repository is a little special. There is no project
40+
# manifest file for it. A specific project must always be explicitly
41+
# specified when building from fbsource.
42+
raise UsageError(
43+
"no project name specified (required when building in fbsource)"
44+
)
45+
args.project = opts.repo_project
46+
47+
ctx_gen = opts.get_context_generator()
48+
if args.test_dependencies:
49+
ctx_gen.set_value_for_all_projects("test", "on")
50+
if args.enable_tests:
51+
ctx_gen.set_value_for_project(args.project, "test", "on")
52+
else:
53+
ctx_gen.set_value_for_project(args.project, "test", "off")
54+
55+
if opts.shared_libs:
56+
ctx_gen.set_value_for_all_projects("shared_libs", "on")
57+
58+
loader = ManifestLoader(opts, ctx_gen)
59+
self.process_project_dir_arguments(args, loader)
60+
61+
manifest = loader.load_manifest(args.project)
62+
63+
return self.run_project_cmd(args, loader, manifest)
64+
65+
def process_project_dir_arguments(self, args, loader):
66+
def parse_project_arg(arg, arg_type):
67+
parts = arg.split(":")
68+
if len(parts) == 2:
69+
project, path = parts
70+
elif len(parts) == 1:
71+
project = args.project
72+
path = parts[0]
73+
# On Windows path contains colon, e.g. C:\open
74+
elif os.name == "nt" and len(parts) == 3:
75+
project = parts[0]
76+
path = parts[1] + ":" + parts[2]
77+
else:
78+
raise UsageError(
79+
"invalid %s argument; too many ':' characters: %s" % (arg_type, arg)
80+
)
81+
82+
return project, os.path.abspath(path)
83+
84+
# If we are currently running from a project repository,
85+
# use the current repository for the project sources.
86+
build_opts = loader.build_opts
87+
if build_opts.repo_project is not None and build_opts.repo_root is not None:
88+
loader.set_project_src_dir(build_opts.repo_project, build_opts.repo_root)
89+
90+
for arg in args.src_dir:
91+
project, path = parse_project_arg(arg, "--src-dir")
92+
loader.set_project_src_dir(project, path)
93+
94+
for arg in args.build_dir:
95+
project, path = parse_project_arg(arg, "--build-dir")
96+
loader.set_project_build_dir(project, path)
97+
98+
for arg in args.install_dir:
99+
project, path = parse_project_arg(arg, "--install-dir")
100+
loader.set_project_install_dir(project, path)
101+
102+
for arg in args.project_install_prefix:
103+
project, path = parse_project_arg(arg, "--install-prefix")
104+
loader.set_project_install_prefix(project, path)
105+
106+
def setup_parser(self, parser):
107+
parser.add_argument(
108+
"project",
109+
nargs="?",
110+
help=(
111+
"name of the project or path to a manifest "
112+
"file describing the project"
113+
),
114+
)
115+
parser.add_argument(
116+
"--no-tests",
117+
action="store_false",
118+
dest="enable_tests",
119+
default=True,
120+
help="Disable building tests for this project.",
121+
)
122+
parser.add_argument(
123+
"--test-dependencies",
124+
action="store_true",
125+
help="Enable building tests for dependencies as well.",
126+
)
127+
parser.add_argument(
128+
"--current-project",
129+
help="Specify the name of the fbcode_builder manifest file for the "
130+
"current repository. If not specified, the code will attempt to find "
131+
"this in a .projectid file in the repository root.",
132+
)
133+
parser.add_argument(
134+
"--src-dir",
135+
default=[],
136+
action="append",
137+
help="Specify a local directory to use for the project source, "
138+
"rather than fetching it.",
139+
)
140+
parser.add_argument(
141+
"--build-dir",
142+
default=[],
143+
action="append",
144+
help="Explicitly specify the build directory to use for the "
145+
"project, instead of the default location in the scratch path. "
146+
"This only affects the project specified, and not its dependencies.",
147+
)
148+
parser.add_argument(
149+
"--install-dir",
150+
default=[],
151+
action="append",
152+
help="Explicitly specify the install directory to use for the "
153+
"project, instead of the default location in the scratch path. "
154+
"This only affects the project specified, and not its dependencies.",
155+
)
156+
parser.add_argument(
157+
"--project-install-prefix",
158+
default=[],
159+
action="append",
160+
help="Specify the final deployment installation path for a project",
161+
)
162+
163+
self.setup_project_cmd_parser(parser)
164+
165+
def setup_project_cmd_parser(self, parser):
166+
pass
167+
168+
def create_builder(self, loader, manifest):
169+
fetcher = loader.create_fetcher(manifest)
170+
src_dir = fetcher.get_src_dir()
171+
ctx = loader.ctx_gen.get_context(manifest.name)
172+
build_dir = loader.get_project_build_dir(manifest)
173+
inst_dir = loader.get_project_install_dir(manifest)
174+
return manifest.create_builder(
175+
loader.build_opts,
176+
src_dir,
177+
build_dir,
178+
inst_dir,
179+
ctx,
180+
loader,
181+
loader.dependencies_of(manifest),
182+
)
183+
184+
def check_built(self, loader, manifest):
185+
built_marker = os.path.join(
186+
loader.get_project_install_dir(manifest), ".built-by-getdeps"
187+
)
188+
return os.path.exists(built_marker)

build/fbcode_builder/getdeps/fetcher.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,7 @@ def filter_strip_marker(dest_name: str, marker: str) -> None:
476476

477477

478478
def list_files_under_dir_newer_than_timestamp(
479-
dir_to_scan: str, ts: int
479+
dir_to_scan: str, ts: float
480480
) -> Iterator[str]:
481481
for root, _dirs, files in os.walk(dir_to_scan):
482482
for src_file in files:

build/fbcode_builder/getdeps/manifest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -973,7 +973,7 @@ def set(self, key: str, value: str | None) -> None:
973973
assert key in self.ALLOWED_VARIABLES or key.startswith("feature_")
974974
self.ctx_dict[key] = value
975975

976-
def features(self) -> set[str]:
976+
def features(self) -> typing.Set[str]:
977977
return {
978978
k[len("feature_") :]
979979
for k, v in self.ctx_dict.items()

0 commit comments

Comments
 (0)