Skip to content
Draft
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
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,9 @@ load-plugins = [
"pylint.extensions.set_membership",
"pylint.extensions.typing",
]
# Disable unsubscriptable-object because Pylint has false positives and this check
# overlaps with mypy's checks. Enable the check when the related issue is resolved:
# https://github.com/pylint-dev/pylint/issues/9549
disable = [
"fixme",
"line-too-long", # Replaced by Flake8 Bugbear B950 check.
Expand All @@ -242,6 +245,7 @@ disable = [
"too-many-return-statements",
"too-many-statements",
"duplicate-code",
"unsubscriptable-object",
]

[tool.pylint.MISCELLANEOUS]
Expand Down
128 changes: 59 additions & 69 deletions src/macaron/build_spec_generator/build_command_patcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,80 +65,74 @@
}


def _patch_commands(
cmds_sequence: Sequence[list[str]],
def _patch_command(
cmd: 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.
) -> CLICommand | None:
"""Patch the build command, using the provided CLICommandParser instances.

For each command in `cmds_sequence`, it will be checked against all CLICommandParser instances until there is
The command will be checked against all CLICommandParser instances to find
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 cmd in cmds_sequence:
# Checking if the command is a valid non-empty list.
if not cmd:
continue
effective_cli_parser = None
for cli_parser in cli_parsers:
if cli_parser.is_build_tool(cmd[0]):
effective_cli_parser = cli_parser
break

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

try:
cli_command = effective_cli_parser.parse(cmd)
except CommandLineParseError as error:
logger.error(
"Failed to patch the cli command %s. Error %s.",
" ".join(cmd),
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(cmd),
error,
)
return None

result.append(new_cli_command)

return result


def patch_commands(
cmds_sequence: Sequence[list[str]],
# Checking if the command is a valid non-empty list.
if not cmd:
return None

effective_cli_parser = None
for cli_parser in cli_parsers:
if cli_parser.is_build_tool(cmd[0]):
effective_cli_parser = cli_parser
break

if not effective_cli_parser:
return UnparsedCLICommand(original_cmds=cmd)

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

patch = patches.get(effective_cli_parser.build_tool, None)
if not patch:
return cli_command

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


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

For each command in this command sequence:
Possible scenarios:

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

Expand All @@ -158,21 +152,17 @@ def patch_commands(

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.
list[str] | None
The patched command or None if there is an error. An error happens if a command,
or the patch value is valid.
"""
result = []
patch_cli_commands = _patch_commands(
cmds_sequence=cmds_sequence,
patch_cli_command = _patch_command(
cmd=cmd,
cli_parsers=[MVN_CLI_PARSER, GRADLE_CLI_PARSER],
patches=patches,
)

if patch_cli_commands is None:
if patch_cli_command is None:
return None

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

return result
return patch_cli_command.to_cmds()
50 changes: 33 additions & 17 deletions src/macaron/build_spec_generator/common_spec/base_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,32 @@
from packageurl import PackageURL


class SpecBuildCommandDict(TypedDict, total=False):
"""
Initialize build command section of the build specification.

It contains helpful information related to a build command.
"""

#: The build tool.
build_tool: Required[str]

#: The build tool version.
build_tool_version: NotRequired[str]

#: The build configuration path
build_config_path: Required[str]

#: The root build configuration path if present
root_build_config_path: NotRequired[str]

#: The build command.
command: Required[list[str]]

#: The confidence score for the analysis result that has inferred the build tool information.
confidence_score: Required[float]


class BaseBuildSpecDict(TypedDict, total=False):
"""
Initialize base build specification.
Expand Down Expand Up @@ -58,8 +84,8 @@ class BaseBuildSpecDict(TypedDict, total=False):
#: 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 and related information to build the project.
build_commands: NotRequired[list[SpecBuildCommandDict]]

#: List of shell commands to test the project.
test_commands: NotRequired[list[list[str]]]
Expand Down Expand Up @@ -103,24 +129,14 @@ def resolve_fields(self, purl: PackageURL) -> None:
"""

@abstractmethod
def get_default_build_commands(
def set_default_build_commands(
self,
build_tool_names: list[str],
) -> list[list[str]]:
build_cmd_spec: SpecBuildCommandDict,
) -> None:
"""Return the default build commands for the build tools.

Parameters
----------
build_tool_names: list[str]
The build tools to get the default build command.

Returns
-------
list[list[str]]
The build command as a list[list[str]].

Raises
------
GenerateBuildSpecError
If there is no default build command available for the specified build tool.
build_cmd_spec: SpecBuildCommandDict
The build command and related information.
"""
Loading
Loading