Skip to content

Commit 8b3c4b5

Browse files
tromaibenmssbehnazh-w
authored
feat: add reproducible central buildspec generation (#1115)
This Pull Request adds a new command called gen-build-spec. This command generates a buildspec, which contains the build related information for a PURL that Macaron has analyzed. The output file will be stored within output/<purl_based_path>/macaron.buildspec, where <purl_based_path> being the directory structure according to the input PackageURL. Signed-off-by: Trong Nhan Mai <trong.nhan.mai@oracle.com> Signed-off-by: Ben Selwyn-Smith <benselwynsmith@googlemail.com> Signed-off-by: behnazh-w <behnaz.hassanshahi@oracle.com> Co-authored-by: Ben Selwyn-Smith <benselwynsmith@googlemail.com> Co-authored-by: behnazh-w <behnaz.hassanshahi@oracle.com>
1 parent 22a4e08 commit 8b3c4b5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+6649
-14
lines changed

scripts/release_scripts/run_macaron.sh

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ while [[ $# -gt 0 ]]; do
283283
entrypoint+=("macaron")
284284
;;
285285
# Parsing commands for macaron entrypoint.
286-
analyze|dump-defaults|verify-policy)
286+
analyze|dump-defaults|verify-policy|gen-build-spec)
287287
command=$1
288288
shift
289289
break
@@ -359,6 +359,19 @@ elif [[ $command == "verify-policy" ]]; then
359359
esac
360360
shift
361361
done
362+
elif [[ $command == "gen-build-spec" ]]; then
363+
while [[ $# -gt 0 ]]; do
364+
case $1 in
365+
-d|--database)
366+
gen_build_spec_arg_database="$2"
367+
shift
368+
;;
369+
*)
370+
rest_command+=("$1")
371+
;;
372+
esac
373+
shift
374+
done
362375
elif [[ $command == "dump-defaults" ]]; then
363376
while [[ $# -gt 0 ]]; do
364377
case $1 in
@@ -512,6 +525,28 @@ if [[ -n "${arg_datalog_policy_file:-}" ]]; then
512525
mount_file "-f/--file" "$datalog_policy_file" "$datalog_policy_file_in_container" "ro,Z"
513526
fi
514527

528+
# MACARON entrypoint - gen-build-spec command argvs
529+
# This is for macaron gen-build-spec command.
530+
# Determine the database path to be mounted into ${MACARON_WORKSPACE}/database/<database_file_name>.
531+
if [[ -n "${gen_build_spec_arg_database:-}" ]]; then
532+
gen_build_spec_database_path="${gen_build_spec_arg_database}"
533+
file_name="$(basename "${gen_build_spec_database_path}")"
534+
gen_build_spec_database_path_in_container="${MACARON_WORKSPACE}/database/${file_name}"
535+
536+
argv_command+=("--database" "$gen_build_spec_database_path_in_container")
537+
mount_file "-d/--database" "$gen_build_spec_database_path" "$gen_build_spec_database_path_in_container" "rw,Z"
538+
fi
539+
540+
# Determine that ~/.gradle/gradle.properties exists to be mounted into ${MACARON_WORKSPACE}/gradle.properties
541+
if [[ -f "$HOME/.gradle/gradle.properties" ]]; then
542+
mounts+=("-v" "$HOME/.gradle/gradle.properties":"${MACARON_WORKSPACE}/gradle.properties:ro,Z")
543+
fi
544+
545+
# Determine that ~/.m2/settings.xml exists to be mounted into ${MACARON_WORKSPACE}/settings.xml
546+
if [[ -f "$HOME/.m2/settings.xml" ]]; then
547+
mounts+=("-v" "$HOME/.m2/settings.xml":"${MACARON_WORKSPACE}/settings.xml:ro,Z")
548+
fi
549+
515550
# Set up proxy.
516551
# We respect the host machine's proxy environment variables.
517552
proxy_var_names=(

src/macaron/__main__.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
from packageurl import PackageURL
1515

1616
import macaron
17+
from macaron.build_spec_generator.build_spec_generator import (
18+
BuildSpecFormat,
19+
gen_build_spec_for_purl,
20+
)
1721
from macaron.config.defaults import create_defaults, load_defaults
1822
from macaron.config.global_config import global_config
1923
from macaron.errors import ConfigurationError
@@ -235,6 +239,47 @@ def verify_policy(verify_policy_args: argparse.Namespace) -> int:
235239
return os.EX_USAGE
236240

237241

242+
def gen_build_spec(gen_build_spec_args: argparse.Namespace) -> int:
243+
"""Generate a build spec containing the build information discovered by Macaron.
244+
245+
Returns
246+
-------
247+
int
248+
Returns os.EX_OK if successful or the corresponding error code on failure.
249+
"""
250+
if not os.path.isfile(gen_build_spec_args.database):
251+
logger.critical("The database file does not exist.")
252+
return os.EX_OSFILE
253+
254+
output_format = gen_build_spec_args.output_format
255+
256+
try:
257+
build_spec_format = BuildSpecFormat(output_format)
258+
except ValueError:
259+
logger.error("The output format %s is not supported.", output_format)
260+
return os.EX_USAGE
261+
262+
try:
263+
purl = PackageURL.from_string(gen_build_spec_args.package_url)
264+
except ValueError as error:
265+
logger.error("Cannot parse purl %s. Error %s", gen_build_spec_args.package_url, error)
266+
return os.EX_USAGE
267+
268+
logger.info(
269+
"Generating %s buildspec for PURL %s from %s.",
270+
output_format,
271+
purl,
272+
gen_build_spec_args.database,
273+
)
274+
275+
return gen_build_spec_for_purl(
276+
purl=purl,
277+
database_path=gen_build_spec_args.database,
278+
build_spec_format=build_spec_format,
279+
output_path=global_config.output_path,
280+
)
281+
282+
238283
def find_source(find_args: argparse.Namespace) -> int:
239284
"""Perform repo and commit finding for a passed PURL, or commit finding for a passed PURL and repo."""
240285
if repo_finder.find_source(find_args.package_url, find_args.repo_path or None):
@@ -283,6 +328,9 @@ def perform_action(action_args: argparse.Namespace) -> None:
283328

284329
find_source(action_args)
285330

331+
case "gen-build-spec":
332+
sys.exit(gen_build_spec(action_args))
333+
286334
case _:
287335
logger.error("Macaron does not support command option %s.", action_args.action)
288336
sys.exit(os.EX_USAGE)
@@ -515,6 +563,30 @@ def main(argv: list[str] | None = None) -> None:
515563
),
516564
)
517565

566+
# Generate a build spec containing rebuild information for a software component.
567+
gen_build_spec_parser = sub_parser.add_parser(name="gen-build-spec")
568+
569+
gen_build_spec_parser.add_argument(
570+
"-purl",
571+
"--package-url",
572+
required=True,
573+
type=str,
574+
help=("The PURL string of the software component to generate build spec for."),
575+
)
576+
577+
gen_build_spec_parser.add_argument(
578+
"--database",
579+
help="Path to the database.",
580+
required=True,
581+
)
582+
583+
gen_build_spec_parser.add_argument(
584+
"--output-format",
585+
type=str,
586+
help=('The output format. Can be rc-buildspec (Reproducible-central build spec) (default "rc-buildspec")'),
587+
default="rc-buildspec",
588+
)
589+
518590
args = main_parser.parse_args(argv)
519591

520592
if not args.action:
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright (c) 2025 - 2025, Oracle and/or its affiliates. All rights reserved.
2+
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/.
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Copyright (c) 2025 - 2025, Oracle and/or its affiliates. All rights reserved.
2+
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/.
3+
4+
"""This module contains the implementation of the build command patching."""
5+
6+
import logging
7+
from collections.abc import Mapping, Sequence
8+
9+
from macaron.build_spec_generator.cli_command_parser import CLICommand, CLICommandParser, PatchCommandBuildTool
10+
from macaron.build_spec_generator.cli_command_parser.gradle_cli_parser import (
11+
GradleCLICommandParser,
12+
GradleOptionPatchValueType,
13+
)
14+
from macaron.build_spec_generator.cli_command_parser.maven_cli_parser import (
15+
CommandLineParseError,
16+
MavenCLICommandParser,
17+
MavenOptionPatchValueType,
18+
PatchBuildCommandError,
19+
)
20+
from macaron.build_spec_generator.cli_command_parser.unparsed_cli_command import UnparsedCLICommand
21+
22+
logger: logging.Logger = logging.getLogger(__name__)
23+
24+
MVN_CLI_PARSER = MavenCLICommandParser()
25+
GRADLE_CLI_PARSER = GradleCLICommandParser()
26+
27+
PatchValueType = GradleOptionPatchValueType | MavenOptionPatchValueType
28+
29+
30+
def _patch_commands(
31+
cmds_sequence: Sequence[list[str]],
32+
cli_parsers: Sequence[CLICommandParser],
33+
patches: Mapping[
34+
PatchCommandBuildTool,
35+
Mapping[str, PatchValueType | None],
36+
],
37+
) -> list[CLICommand] | None:
38+
"""Patch the sequence of build commands, using the provided CLICommandParser instances.
39+
40+
For each command in `cmds_sequence`, it will be checked against all CLICommandParser instances until there is
41+
one that can parse it, then a patch from ``patches`` is applied for this command if provided.
42+
43+
If a command doesn't have any corresponding ``CLICommandParser`` instance it will be parsed as UnparsedCLICommand,
44+
which just holds the original command as a list of string, without any changes.
45+
"""
46+
result: list[CLICommand] = []
47+
for cmds in cmds_sequence:
48+
effective_cli_parser = None
49+
for cli_parser in cli_parsers:
50+
if cli_parser.is_build_tool(cmds[0]):
51+
effective_cli_parser = cli_parser
52+
break
53+
54+
if not effective_cli_parser:
55+
result.append(UnparsedCLICommand(original_cmds=cmds))
56+
continue
57+
58+
try:
59+
cli_command = effective_cli_parser.parse(cmds)
60+
except CommandLineParseError as error:
61+
logger.error(
62+
"Failed to patch the cli command %s. Error %s.",
63+
" ".join(cmds),
64+
error,
65+
)
66+
return None
67+
68+
patch = patches.get(effective_cli_parser.build_tool, None)
69+
if not patch:
70+
result.append(cli_command)
71+
continue
72+
73+
try:
74+
new_cli_command = effective_cli_parser.apply_patch(
75+
cli_command=cli_command,
76+
patch_options=patch,
77+
)
78+
except PatchBuildCommandError as error:
79+
logger.error(
80+
"Failed to patch the build command %s. Error %s.",
81+
" ".join(cmds),
82+
error,
83+
)
84+
return None
85+
86+
result.append(new_cli_command)
87+
88+
return result
89+
90+
91+
def patch_commands(
92+
cmds_sequence: Sequence[list[str]],
93+
patches: Mapping[
94+
PatchCommandBuildTool,
95+
Mapping[str, PatchValueType | None],
96+
],
97+
) -> list[list[str]] | None:
98+
"""Patch a sequence of CLI commands.
99+
100+
For each command in this command sequence:
101+
102+
- If the command is not a build command, or it's a tool we do not support, it will be left intact.
103+
104+
- If the command is a build command we support, it will be patched, if a patch value is provided in ``patches``.
105+
If no patch value is provided for a build command, it will be left intact.
106+
107+
`patches` is a mapping with:
108+
109+
- **Key**: an instance of the ``BuildTool`` enum
110+
111+
- **Value**: the patch value provided to ``CLICommandParser.apply_patch``. For more information on the patch value
112+
see the concrete implementations of the ``CLICommandParser.apply_patch`` method.
113+
For example: :class:`macaron.cli_command_parser.maven_cli_parser.MavenCLICommandParser.apply_patch`,
114+
:class:`macaron.cli_command_parser.gradle_cli_parser.GradleCLICommandParser.apply_patch`.
115+
116+
This means that all commands that match a BuildTool will be applied by the same patch value.
117+
118+
Returns
119+
-------
120+
list[list[str]] | None
121+
The patched command sequence or None if there is an error. The errors that can happen if any command
122+
which we support is invalid in ``cmds_sequence``, or the patch value is valid.
123+
"""
124+
result = []
125+
patch_cli_commands = _patch_commands(
126+
cmds_sequence=cmds_sequence,
127+
cli_parsers=[MVN_CLI_PARSER, GRADLE_CLI_PARSER],
128+
patches=patches,
129+
)
130+
131+
if patch_cli_commands is None:
132+
return None
133+
134+
for patch_cmd in patch_cli_commands:
135+
result.append(patch_cmd.to_cmds())
136+
137+
return result

0 commit comments

Comments
 (0)