Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a7a282a
feat: add macaron database extractor module
Jul 9, 2025
ed57cc4
feat: add maven and gradle cli parsers
Jul 9, 2025
4aa3743
feat: add jdk version finder from maven central java artifacts
Jul 9, 2025
dcae8e5
feat: add cli build command patcher
Jul 9, 2025
8909a01
feat: add jdk version normalizer
Jul 9, 2025
a8662f4
feat: add reproducible central buildspec generation
Jul 9, 2025
426e303
feat: expose macaron gen-build-spec cli command
Jul 9, 2025
f24d7af
test: modify the integration test script to use the compare rc build …
Jul 9, 2025
4c3ef2d
test: add integration tests for the gen-build-spec error
Jul 9, 2025
779c03b
fix: add jdk version 22 and 23 into the list of supported jdk major v…
Jul 18, 2025
d0c10de
chore: add a small log message at the beginning of build spec generation
Jul 18, 2025
0f9b0c6
fix: fix the sql statement for obtaining build check facts where the …
Jul 21, 2025
15efd52
chore: move the looking up of repository before the build tool lookup…
Jul 21, 2025
787e9b3
chore: support gen-build-spec for the Docker image
Jul 21, 2025
7d548bb
fix: use the correct analysis_id foreign key to map with Analaysis in…
Aug 5, 2025
86adc3b
chore: simplify the looking up component information by getting the I…
Aug 6, 2025
c14d8dc
refactor: refactor the function to get the purl-based directory path …
Aug 6, 2025
2bfc3b2
test: improve test_macaron_db_extractor test module
Aug 6, 2025
543246e
feat: generate build spec into a purl-based path in the output directory
Aug 6, 2025
5a89639
feat: always prioritize jdk version obtained from JAR from maven central
Aug 6, 2025
d339e50
chore: fix typos
Aug 6, 2025
95908b4
chore: remove extra comments feature and some refactoring to simplify…
Aug 8, 2025
1ee8a19
chore: misc fixes
benmss Sep 7, 2025
5a7923b
chore: address PR feedback
behnazh-w Sep 10, 2025
71b9f5a
chore: address PR feedback
behnazh-w Sep 12, 2025
a9be2e3
chore: address PR feedback
behnazh-w Sep 15, 2025
211c23e
docs: fix the docstrings and comments
behnazh-w Sep 16, 2025
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
37 changes: 36 additions & 1 deletion scripts/release_scripts/run_macaron.sh
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ while [[ $# -gt 0 ]]; do
entrypoint+=("macaron")
;;
# Parsing commands for macaron entrypoint.
analyze|dump-defaults|verify-policy)
analyze|dump-defaults|verify-policy|gen-build-spec)
command=$1
shift
break
Expand Down Expand Up @@ -359,6 +359,19 @@ elif [[ $command == "verify-policy" ]]; then
esac
shift
done
elif [[ $command == "gen-build-spec" ]]; then
while [[ $# -gt 0 ]]; do
case $1 in
-d|--database)
gen_build_spec_arg_database="$2"
shift
;;
*)
rest_command+=("$1")
;;
esac
shift
done
elif [[ $command == "dump-defaults" ]]; then
while [[ $# -gt 0 ]]; do
case $1 in
Expand Down Expand Up @@ -512,6 +525,28 @@ if [[ -n "${arg_datalog_policy_file:-}" ]]; then
mount_file "-f/--file" "$datalog_policy_file" "$datalog_policy_file_in_container" "ro,Z"
fi

# MACARON entrypoint - gen-build-spec command argvs
# This is for macaron gen-build-spec command.
# Determine the database path to be mounted into ${MACARON_WORKSPACE}/database/<database_file_name>.
if [[ -n "${gen_build_spec_arg_database:-}" ]]; then
gen_build_spec_database_path="${gen_build_spec_arg_database}"
file_name="$(basename "${gen_build_spec_database_path}")"
gen_build_spec_database_path_in_container="${MACARON_WORKSPACE}/database/${file_name}"

argv_command+=("--database" "$gen_build_spec_database_path_in_container")
mount_file "-d/--database" "$gen_build_spec_database_path" "$gen_build_spec_database_path_in_container" "rw,Z"
fi

# Determine that ~/.gradle/gradle.properties exists to be mounted into ${MACARON_WORKSPACE}/gradle.properties
if [[ -f "$HOME/.gradle/gradle.properties" ]]; then
mounts+=("-v" "$HOME/.gradle/gradle.properties":"${MACARON_WORKSPACE}/gradle.properties:ro,Z")
fi

# Determine that ~/.m2/settings.xml exists to be mounted into ${MACARON_WORKSPACE}/settings.xml
if [[ -f "$HOME/.m2/settings.xml" ]]; then
mounts+=("-v" "$HOME/.m2/settings.xml":"${MACARON_WORKSPACE}/settings.xml:ro,Z")
fi

# Set up proxy.
# We respect the host machine's proxy environment variables.
proxy_var_names=(
Expand Down
72 changes: 72 additions & 0 deletions src/macaron/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
from packageurl import PackageURL

import macaron
from macaron.build_spec_generator.build_spec_generator import (
BuildSpecFormat,
gen_build_spec_for_purl,
)
from macaron.config.defaults import create_defaults, load_defaults
from macaron.config.global_config import global_config
from macaron.errors import ConfigurationError
Expand Down Expand Up @@ -235,6 +239,47 @@ def verify_policy(verify_policy_args: argparse.Namespace) -> int:
return os.EX_USAGE


def gen_build_spec(gen_build_spec_args: argparse.Namespace) -> int:
"""Generate a build spec containing the build information discovered by Macaron.

Returns
-------
int
Returns os.EX_OK if successful or the corresponding error code on failure.
"""
if not os.path.isfile(gen_build_spec_args.database):
logger.critical("The database file does not exist.")
return os.EX_OSFILE

output_format = gen_build_spec_args.output_format

try:
build_spec_format = BuildSpecFormat(output_format)
except ValueError:
logger.error("The output format %s is not supported.", output_format)
return os.EX_USAGE

try:
purl = PackageURL.from_string(gen_build_spec_args.package_url)
except ValueError as error:
logger.error("Cannot parse purl %s. Error %s", gen_build_spec_args.package_url, error)
return os.EX_USAGE

logger.info(
"Generating %s buildspec for PURL %s from %s.",
output_format,
purl,
gen_build_spec_args.database,
)

return gen_build_spec_for_purl(
purl=purl,
database_path=gen_build_spec_args.database,
build_spec_format=build_spec_format,
output_path=global_config.output_path,
)


def find_source(find_args: argparse.Namespace) -> int:
"""Perform repo and commit finding for a passed PURL, or commit finding for a passed PURL and repo."""
if repo_finder.find_source(find_args.package_url, find_args.repo_path or None):
Expand Down Expand Up @@ -283,6 +328,9 @@ def perform_action(action_args: argparse.Namespace) -> None:

find_source(action_args)

case "gen-build-spec":
sys.exit(gen_build_spec(action_args))

case _:
logger.error("Macaron does not support command option %s.", action_args.action)
sys.exit(os.EX_USAGE)
Expand Down Expand Up @@ -515,6 +563,30 @@ def main(argv: list[str] | None = None) -> None:
),
)

# Generate a build spec containing rebuild information for a software component.
gen_build_spec_parser = sub_parser.add_parser(name="gen-build-spec")

gen_build_spec_parser.add_argument(
"-purl",
"--package-url",
required=True,
type=str,
help=("The PURL string of the software component to generate build spec for."),
)

gen_build_spec_parser.add_argument(
"--database",
help="Path to the database.",
required=True,
)

gen_build_spec_parser.add_argument(
"--output-format",
type=str,
help=('The output format. Can be rc-buildspec (Reproducible-central build spec) (default "rc-buildspec")'),
default="rc-buildspec",
)

args = main_parser.parse_args(argv)

if not args.action:
Expand Down
2 changes: 2 additions & 0 deletions src/macaron/build_spec_generator/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright (c) 2025 - 2025, Oracle and/or its affiliates. All rights reserved.
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/.
137 changes: 137 additions & 0 deletions src/macaron/build_spec_generator/build_command_patcher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# Copyright (c) 2025 - 2025, Oracle and/or its affiliates. All rights reserved.
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/.

"""This module contains the implementation of the build command patching."""

import logging
from collections.abc import Mapping, Sequence

from macaron.build_spec_generator.cli_command_parser import CLICommand, CLICommandParser, PatchCommandBuildTool
from macaron.build_spec_generator.cli_command_parser.gradle_cli_parser import (
GradleCLICommandParser,
GradleOptionPatchValueType,
)
from macaron.build_spec_generator.cli_command_parser.maven_cli_parser import (
CommandLineParseError,
MavenCLICommandParser,
MavenOptionPatchValueType,
PatchBuildCommandError,
)
from macaron.build_spec_generator.cli_command_parser.unparsed_cli_command import UnparsedCLICommand

logger: logging.Logger = logging.getLogger(__name__)

MVN_CLI_PARSER = MavenCLICommandParser()
GRADLE_CLI_PARSER = GradleCLICommandParser()

PatchValueType = GradleOptionPatchValueType | MavenOptionPatchValueType


def _patch_commands(
cmds_sequence: Sequence[list[str]],
cli_parsers: Sequence[CLICommandParser],
patches: Mapping[
PatchCommandBuildTool,
Mapping[str, PatchValueType | None],
],
) -> list[CLICommand] | None:
"""Patch the sequence of build commands, using the provided CLICommandParser instances.

For each command in `cmds_sequence`, it will be checked against all CLICommandParser instances until there is
one that can parse it, then a patch from ``patches`` is applied for this command if provided.

If a command doesn't have any corresponding ``CLICommandParser`` instance it will be parsed as UnparsedCLICommand,
which just holds the original command as a list of string, without any changes.
"""
result: list[CLICommand] = []
for cmds in cmds_sequence:
effective_cli_parser = None
for cli_parser in cli_parsers:
if cli_parser.is_build_tool(cmds[0]):
effective_cli_parser = cli_parser
break

if not effective_cli_parser:
result.append(UnparsedCLICommand(original_cmds=cmds))
continue

try:
cli_command = effective_cli_parser.parse(cmds)
except CommandLineParseError as error:
logger.error(
"Failed to patch the cli command %s. Error %s.",
" ".join(cmds),
error,
)
return None

patch = patches.get(effective_cli_parser.build_tool, None)
if not patch:
result.append(cli_command)
continue

try:
new_cli_command = effective_cli_parser.apply_patch(
cli_command=cli_command,
patch_options=patch,
)
except PatchBuildCommandError as error:
logger.error(
"Failed to patch the build command %s. Error %s.",
" ".join(cmds),
error,
)
return None

result.append(new_cli_command)

return result


def patch_commands(
cmds_sequence: Sequence[list[str]],
patches: Mapping[
PatchCommandBuildTool,
Mapping[str, PatchValueType | None],
],
) -> list[list[str]] | None:
"""Patch a sequence of CLI commands.

For each command in this command sequence:

- If the command is not a build command, or it's a tool we do not support, it will be left intact.

- If the command is a build command we support, it will be patched, if a patch value is provided in ``patches``.
If no patch value is provided for a build command, it will be left intact.

`patches` is a mapping with:

- **Key**: an instance of the ``BuildTool`` enum

- **Value**: the patch value provided to ``CLICommandParser.apply_patch``. For more information on the patch value
see the concrete implementations of the ``CLICommandParser.apply_patch`` method.
For example: :class:`macaron.cli_command_parser.maven_cli_parser.MavenCLICommandParser.apply_patch`,
:class:`macaron.cli_command_parser.gradle_cli_parser.GradleCLICommandParser.apply_patch`.

This means that all commands that match a BuildTool will be applied by the same patch value.

Returns
-------
list[list[str]] | None
The patched command sequence or None if there is an error. The errors that can happen if any command
which we support is invalid in ``cmds_sequence``, or the patch value is valid.
"""
result = []
patch_cli_commands = _patch_commands(
cmds_sequence=cmds_sequence,
cli_parsers=[MVN_CLI_PARSER, GRADLE_CLI_PARSER],
patches=patches,
)

if patch_cli_commands is None:
return None

for patch_cmd in patch_cli_commands:
result.append(patch_cmd.to_cmds())

return result
Loading
Loading