Skip to content
Open
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
6 changes: 4 additions & 2 deletions src/macaron/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -618,8 +618,10 @@ def main(argv: list[str] | None = None) -> None:
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",
help=(
"The output format. Can be default-buildspec (default) or rc-buildspec (Reproducible-central build spec)"
),
default="default-buildspec",
)

args = main_parser.parse_args(argv)
Expand Down
72 changes: 47 additions & 25 deletions src/macaron/build_spec_generator/build_spec_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

"""This module contains the functions used for generating build specs from the Macaron database."""

import json
import logging
import os
from collections.abc import Mapping
Expand All @@ -13,8 +14,10 @@
from sqlalchemy.orm import Session

from macaron.build_spec_generator.build_command_patcher import PatchCommandBuildTool, PatchValueType
from macaron.build_spec_generator.common_spec.core import gen_generic_build_spec
from macaron.build_spec_generator.reproducible_central.reproducible_central import gen_reproducible_central_build_spec
from macaron.console import access_handler
from macaron.errors import GenerateBuildSpecError
from macaron.path_utils.purl_based_path import get_purl_based_dir

logger: logging.Logger = logging.getLogger(__name__)
Expand All @@ -25,6 +28,8 @@ class BuildSpecFormat(str, Enum):

REPRODUCIBLE_CENTRAL = "rc-buildspec"

DEFAULT = "default-buildspec"


CLI_COMMAND_PATCHES: dict[
PatchCommandBuildTool,
Expand Down Expand Up @@ -96,51 +101,68 @@ def gen_build_spec_for_purl(
db_engine = create_engine(f"sqlite+pysqlite:///file:{database_path}?mode=ro&uri=true", echo=False)
build_spec_content = None

build_spec_dir_path = os.path.join(
output_path,
"buildspec",
get_purl_based_dir(
purl_name=purl.name,
purl_namespace=purl.namespace,
purl_type=purl.type,
),
)

with Session(db_engine) as session, session.begin():
try:
build_spec = gen_generic_build_spec(purl=purl, session=session, patches=CLI_COMMAND_PATCHES)
except GenerateBuildSpecError as error:
logger.error("Error while generating the build spec: %s.", error)
return os.EX_DATAERR
match build_spec_format:
case BuildSpecFormat.REPRODUCIBLE_CENTRAL:
build_spec_content = gen_reproducible_central_build_spec(
purl=purl,
session=session,
patches=CLI_COMMAND_PATCHES,
)
try:
build_spec_content = gen_reproducible_central_build_spec(build_spec)
except GenerateBuildSpecError as error:
logger.error("Error while generating the build spec: %s.", error)
return os.EX_DATAERR
build_spec_file_path = os.path.join(build_spec_dir_path, "reproducible_central.buildspec")
# Default build spec.
case BuildSpecFormat.DEFAULT:
try:
build_spec_content = json.dumps(build_spec)
except ValueError as error:
logger.error("Error while serializing the build spec: %s.", error)
return os.EX_DATAERR
build_spec_file_path = os.path.join(build_spec_dir_path, "macaron.buildspec")

if not build_spec_content:
logger.error("Error while generating the build spec.")
return os.EX_DATAERR

logger.debug("Build spec content: \n%s", build_spec_content)

build_spec_filepath = os.path.join(
output_path,
"buildspec",
get_purl_based_dir(
purl_name=purl.name,
purl_namespace=purl.namespace,
purl_type=purl.type,
),
"macaron.buildspec",
)

os.makedirs(
name=os.path.dirname(build_spec_filepath),
exist_ok=True,
)
try:
os.makedirs(
name=build_spec_dir_path,
exist_ok=True,
)
except OSError as error:
logger.error("Unable to create the output file: %s.", error)
return os.EX_OSERR

logger.info(
"Generating the %s format build spec to %s.",
"Generating the %s format build spec to %s",
build_spec_format.value,
os.path.relpath(build_spec_filepath, os.getcwd()),
os.path.relpath(build_spec_file_path, os.getcwd()),
)
rich_handler = access_handler.get_handler()
rich_handler.update_gen_build_spec("Build Spec Path:", os.path.relpath(build_spec_filepath, os.getcwd()))
rich_handler.update_gen_build_spec("Build Spec Path:", os.path.relpath(build_spec_file_path, os.getcwd()))
try:
with open(build_spec_filepath, mode="w", encoding="utf-8") as file:
with open(build_spec_file_path, mode="w", encoding="utf-8") as file:
file.write(build_spec_content)
except OSError as error:
logger.error(
"Could not create the build spec at %s. Error: %s",
os.path.relpath(build_spec_filepath, os.getcwd()),
os.path.relpath(build_spec_file_path, os.getcwd()),
error,
)
return os.EX_OSERR
Expand Down
2 changes: 2 additions & 0 deletions src/macaron/build_spec_generator/common_spec/__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/.
89 changes: 89 additions & 0 deletions src/macaron/build_spec_generator/common_spec/base_spec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# 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 includes base build specification and helper classes."""

from abc import ABC, abstractmethod
from typing import NotRequired, Required, TypedDict

from packageurl import PackageURL


class BaseBuildSpecDict(TypedDict, total=False):
"""
Initialize base build specification.

It supports multiple languages, build tools, and additional metadata for enhanced traceability.
"""

#: The package ecosystem.
ecosystem: Required[str]

#: The package identifier.
purl: Required[str]

#: The programming language, e.g., 'java', 'python', 'javascript'.
language: Required[str]

#: The build tool or package manager, e.g., 'maven', 'gradle', 'pip', 'poetry', 'npm', 'yarn'.
build_tool: Required[str]

#: The version of Macaron used for generating the spec.
macaron_version: Required[str]

#: The group identifier for the project/component.
group_id: NotRequired[str | None]

#: The artifact identifier for the project/component.
artifact_id: Required[str]

#: The version of the package or component.
version: Required[str]

#: The remote path or URL of the git repository.
git_repo: NotRequired[str]

#: The commit SHA or tag in the VCS repository.
git_tag: NotRequired[str]

#: The type of line endings used (e.g., 'lf', 'crlf').
newline: NotRequired[str]

#: The version of the programming language or runtime, e.g., '11' for JDK, '3.11' for Python.
language_version: Required[str]

#: List of release dependencies.
dependencies: NotRequired[list[str]]

#: List of build dependencies, which includes tests.
build_dependencies: NotRequired[list[str]]

#: List of shell commands to build the project.
build_commands: NotRequired[list[list[str]]]

#: List of shell commands to test the project.
test_commands: NotRequired[list[str]]

#: Environment variables required during build or test.
environment: NotRequired[dict[str, str]]

#: Path or location of the build artifact/output.
artifact_path: NotRequired[str | None]

#: Entry point script, class, or binary for running the project.
entry_point: NotRequired[str | None]


class BaseBuildSpec(ABC):
"""Abstract base class for build specification behavior and field resolution."""

@abstractmethod
def resolve_fields(self, purl: PackageURL) -> None:
"""
Resolve fields that require special logic for a specific build ecosystem.

Notes
-----
This method should be implemented by subclasses to handle
logic specific to a given package ecosystem, such as Maven or PyPI.
"""
Loading
Loading