generated from canonical/starbase
-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat!(services): add a project service #646
Merged
Merged
Changes from all commits
Commits
Show all changes
42 commits
Select commit
Hold shift + click to select a range
04bc7b4
feat!(partial): project service
lengau 231ba21
refactor(tests/fixtures): create a single default fake project
lengau 7b01dfa
feat(revert): fix service factory removal of attributes
lengau 1cada53
refactor(ServiceFactory): de-dataclass the ServiceFactory
lengau 89563c9
fix(services): make the FetchService use the ProjectService
lengau 4f29d58
fix(tests): fix InitService tests for fixture changes
lengau 9e0c751
fix(lifecycle): make LifecycleService no longer a ProjectService
lengau a1857c7
fix(services/package): make PackageService use ProjectService
lengau 6644cf2
fix(services): make ProviderService use ProjectService
lengau a7ae8c5
fix(tests): fix request service test
lengau cd52083
fix(services): make RemoteBuildService use ProjectService
lengau 522716a
fix(tests): fix the service factory tests
lengau b2a37b7
test(ProjectService): add expand_environment tests for the ProjectSer…
lengau 7cc0653
test: add more unit tests for ProjectService
lengau cc23eb7
feat!(secrets): remove secrets
lengau fb7dd89
chore!(application): remove yaml transforms and environment from app
lengau 79b8ee2
test: add integration tests for rendering a project
lengau 8008304
fix(application): fix some application tests
lengau adb77bc
test: remove unneeded tests
lengau 87cb9d0
test: move adopt-info test to project service
lengau 7eff14d
test: ignore unnecessary test
lengau 9502d62
fix(test): fix application tests
lengau 463b75c
fix(remote-build): make the remote build command use the project service
lengau 176f37e
build(deps): only use python-apt-wheels repo for python-apt
lengau 232cbfa
fix(tests/lifecycle): set the fake project before running lifecycle i…
lengau 888de79
test(lifecycle): correctly set up the package service for lifecycle i…
lengau 5260738
fix(tests): fix the service factory tests for new fixtures
lengau a481640
feat(application): make the application bootstrap early-run services
lengau 6c811b9
test: fix or skip application integration tests
lengau 9bf69f5
test: don't pass a project to the provider service
lengau 20842fa
test: don't pass a project to the fetch service
lengau ad23ccf
chore: eliminate the last vestiges of the old ProjectService
lengau 571ff81
style: fix linting issues
lengau 07de5cb
chore: revert accidental commit of common.mk change
lengau e7acb23
fix(ProjectService): mark most methods as final
lengau e513598
fix(lint): fix linter issue
lengau 39cd8a9
fix(tests): ensure tests don't have side effects
lengau ccdd3fd
fix: pr suggestions
lengau e38da2d
fix: pr feedback
lengau 4fc04fc
fix: add changelog entries
lengau 5026b63
fix: tests that used the old name of configure_early_services
lengau 7aca291
Merge branch 'feature/hybrid-commands' into work/project-service
lengau File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,6 +25,7 @@ | |
import subprocess | ||
import sys | ||
import traceback | ||
import warnings | ||
from collections.abc import Iterable, Sequence | ||
from dataclasses import dataclass, field | ||
from functools import cached_property | ||
|
@@ -37,9 +38,9 @@ | |
from craft_parts.plugins.plugins import PluginType | ||
from platformdirs import user_cache_path | ||
|
||
from craft_application import _config, commands, errors, grammar, models, util | ||
from craft_application import _config, commands, errors, models, util | ||
from craft_application.errors import PathInvalidError | ||
from craft_application.models import BuildInfo, GrammarAwareProject | ||
from craft_application.models import BuildInfo | ||
|
||
if TYPE_CHECKING: | ||
from craft_application.services import service_factory | ||
|
@@ -300,6 +301,17 @@ def cache_dir(self) -> pathlib.Path: | |
f"Unable to create/access cache directory: {err.strerror}" | ||
) from err | ||
|
||
def _configure_early_services(self) -> None: | ||
"""Configure early-starting services. | ||
|
||
This should only contain configuration for services that are needed during | ||
application startup. All other configuration belongs in ``_configure_services`` | ||
""" | ||
self.services.update_kwargs( | ||
"project", | ||
project_dir=self.project_dir, | ||
) | ||
|
||
def _configure_services(self, provider_name: str | None) -> None: | ||
"""Configure additional keyword arguments for any service classes. | ||
|
||
|
@@ -325,18 +337,6 @@ def _configure_services(self, provider_name: str | None) -> None: | |
session_policy=self._fetch_service_policy, | ||
) | ||
|
||
def _resolve_project_path(self, project_dir: pathlib.Path | None) -> pathlib.Path: | ||
"""Find the project file for the current project. | ||
|
||
The default implementation simply looks for the project file in the project | ||
directory. Applications may wish to override this if the project file could be | ||
in multiple places within the project directory. | ||
""" | ||
if project_dir is None: | ||
project_dir = self.project_dir | ||
|
||
return (project_dir / f"{self.app.name}.yaml").resolve(strict=True) | ||
|
||
def get_project( | ||
self, | ||
*, | ||
|
@@ -352,66 +352,17 @@ def get_project( | |
:param build_for: the architecture to build this project for. | ||
:returns: A transformed, loaded project model. | ||
""" | ||
if self.__project is not None: | ||
return self.__project | ||
|
||
try: | ||
project_path = self._resolve_project_path(self.project_dir) | ||
except FileNotFoundError as err: | ||
raise errors.ProjectFileMissingError( | ||
f"Project file '{self.app.name}.yaml' not found in '{self.project_dir}'.", | ||
details="The project file could not be found.", | ||
resolution="Ensure the project file exists.", | ||
retcode=os.EX_NOINPUT, | ||
) from err | ||
craft_cli.emit.debug(f"Loading project file '{project_path!s}'") | ||
|
||
with project_path.open() as file: | ||
yaml_data = util.safe_yaml_load(file) | ||
|
||
host_arch = util.get_host_architecture() | ||
build_planner = self.app.BuildPlannerClass.from_yaml_data( | ||
yaml_data, project_path | ||
) | ||
self._full_build_plan = build_planner.get_build_plan() | ||
self._build_plan = filter_plan( | ||
self._full_build_plan, platform, build_for, host_arch | ||
warnings.warn( | ||
DeprecationWarning( | ||
"Do not get the project directly from the Application. " | ||
"Get it from the project service." | ||
), | ||
stacklevel=2, | ||
) | ||
|
||
if not build_for: | ||
# get the build-for arch from the platform | ||
if platform: | ||
all_platforms = {b.platform: b for b in self._full_build_plan} | ||
if platform not in all_platforms: | ||
raise errors.InvalidPlatformError( | ||
platform, list(all_platforms.keys()) | ||
) | ||
build_for = all_platforms[platform].build_for | ||
# otherwise get the build-for arch from the build plan | ||
elif self._build_plan: | ||
build_for = self._build_plan[0].build_for | ||
|
||
# validate project grammar | ||
GrammarAwareProject.validate_grammar(yaml_data) | ||
|
||
build_on = host_arch | ||
|
||
# Setup partitions, some projects require the yaml data, most will not | ||
self._partitions = self._setup_partitions(yaml_data) | ||
yaml_data = self._transform_project_yaml(yaml_data, build_on, build_for) | ||
self.__project = self.app.ProjectClass.from_yaml_data(yaml_data, project_path) | ||
|
||
# check if mandatory adoptable fields exist if adopt-info not used | ||
for name in self.app.mandatory_adoptable_fields: | ||
if ( | ||
not getattr(self.__project, name, None) | ||
and not self.__project.adopt_info | ||
): | ||
raise errors.CraftValidationError( | ||
f"Required field '{name}' is not set and 'adopt-info' not used." | ||
) | ||
|
||
return self.__project | ||
project_service = self.services.get("project") | ||
if project_service.is_rendered: | ||
return project_service.get() | ||
return project_service.render_once(platform=platform, build_for=build_for) | ||
|
||
@cached_property | ||
def project(self) -> models.Project: | ||
|
@@ -609,7 +560,7 @@ def get_arg_or_config(self, parsed_args: argparse.Namespace, item: str) -> Any: | |
arg_value = getattr(parsed_args, item, None) | ||
if arg_value is not None: | ||
return arg_value | ||
return self.services.config.get(item) | ||
return self.services.get("config").get(item) | ||
|
||
def _run_inner(self) -> int: | ||
"""Actual run implementation.""" | ||
|
@@ -629,18 +580,18 @@ def _run_inner(self) -> int: | |
platform = platform.split(",", maxsplit=1)[0] | ||
if build_for and "," in build_for: | ||
build_for = build_for.split(",", maxsplit=1)[0] | ||
if command.needs_project(dispatcher.parsed_args()): | ||
project_service = self.services.get("project") | ||
# This branch always runs, except during testing. | ||
if not project_service.is_rendered: | ||
project_service.render_once() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what about the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, thanks! |
||
|
||
provider_name = command.provider_name(dispatcher.parsed_args()) | ||
|
||
craft_cli.emit.debug(f"Build plan: platform={platform}, build_for={build_for}") | ||
self._pre_run(dispatcher) | ||
|
||
managed_mode = command.run_managed(dispatcher.parsed_args()) | ||
if managed_mode or command.needs_project(dispatcher.parsed_args()): | ||
self.services.project = self.get_project( | ||
platform=platform, build_for=build_for | ||
) | ||
|
||
self._configure_services(provider_name) | ||
|
||
return_code = 1 # General error | ||
|
@@ -661,6 +612,7 @@ def _run_inner(self) -> int: | |
def run(self) -> int: | ||
"""Bootstrap and run the application.""" | ||
self._setup_logging() | ||
self._configure_early_services() | ||
self._load_plugins() | ||
self._initialize_craft_parts() | ||
|
||
|
@@ -719,78 +671,6 @@ def _emit_error( | |
|
||
craft_cli.emit.error(error) | ||
|
||
def _transform_project_yaml( | ||
self, yaml_data: dict[str, Any], build_on: str, build_for: str | None | ||
) -> dict[str, Any]: | ||
"""Update the project's yaml data with runtime properties. | ||
|
||
Performs task such as environment expansion. Note that this transforms | ||
``yaml_data`` in-place. | ||
""" | ||
# apply application-specific transformations first because an application may | ||
# add advanced grammar, project variables, or secrets to the yaml | ||
yaml_data = self._extra_yaml_transform( | ||
yaml_data, build_on=build_on, build_for=build_for | ||
) | ||
|
||
# At the moment there is no perfect solution for what do to do | ||
# expand project variables or to resolve the grammar if there's | ||
# no explicitly-provided target arch. However, we must resolve | ||
# it with *something* otherwise we might have an invalid parts | ||
# definition full of grammar declarations and incorrect build_for | ||
# architectures. | ||
build_for = build_for or build_on | ||
|
||
# Perform variable expansion. | ||
self._expand_environment(yaml_data=yaml_data, build_for=build_for) | ||
|
||
# Expand grammar. | ||
if "parts" in yaml_data: | ||
craft_cli.emit.debug(f"Processing grammar (on {build_on} for {build_for})") | ||
yaml_data["parts"] = grammar.process_parts( | ||
parts_yaml_data=yaml_data["parts"], | ||
arch=build_on, | ||
target_arch=build_for, | ||
) | ||
|
||
return yaml_data | ||
|
||
def _expand_environment(self, yaml_data: dict[str, Any], build_for: str) -> None: | ||
"""Perform expansion of project environment variables. | ||
|
||
:param yaml_data: The project's yaml data. | ||
:param build_for: The architecture to build for. | ||
""" | ||
if build_for == "all": | ||
build_for_arch = util.get_host_architecture() | ||
craft_cli.emit.debug( | ||
"Expanding environment variables with the host architecture " | ||
f"{build_for_arch!r} as the build-for architecture because 'all' was " | ||
"specified." | ||
) | ||
else: | ||
build_for_arch = build_for | ||
|
||
environment_vars = self._get_project_vars(yaml_data) | ||
project_dirs = craft_parts.ProjectDirs( | ||
work_dir=self._work_dir, partitions=self._partitions | ||
) | ||
|
||
info = craft_parts.ProjectInfo( | ||
application_name=self.app.name, # not used in environment expansion | ||
cache_dir=pathlib.Path(), # not used in environment expansion | ||
arch=build_for_arch, | ||
parallel_build_count=util.get_parallel_build_count(self.app.name), | ||
project_name=yaml_data.get("name", ""), | ||
project_dirs=project_dirs, | ||
project_vars=environment_vars, | ||
partitions=self._partitions, | ||
) | ||
|
||
self._set_global_environment(info) | ||
|
||
craft_parts.expand_environment(yaml_data, info=info) | ||
|
||
def _setup_partitions(self, yaml_data: dict[str, Any]) -> list[str] | None: | ||
"""Return partitions to be used. | ||
|
||
|
@@ -815,19 +695,6 @@ def _set_global_environment(self, info: craft_parts.ProjectInfo) -> None: | |
} | ||
) | ||
|
||
def _extra_yaml_transform( | ||
self, | ||
yaml_data: dict[str, Any], | ||
*, | ||
build_on: str, # noqa: ARG002 (Unused method argument) | ||
build_for: str | None, # noqa: ARG002 (Unused method argument) | ||
) -> dict[str, Any]: | ||
"""Perform additional transformations on a project's yaml data. | ||
|
||
Note: subclasses should return a new dict and keep the parameter unmodified. | ||
""" | ||
return yaml_data | ||
|
||
def _setup_logging(self) -> None: | ||
"""Initialize the logging system.""" | ||
# Set the logging level to DEBUG for all craft-libraries. This is OK even if | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just to double-check: is snapcraft expected to just drop the
override
on their version? Or are there more changes?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Snapcraft will need to move that override to the ProjectService's `resolve_project_file_path()'. Should work roughly the same though (just has to use the class attribute for project dir rather than a method arg)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My concern is that Snapcraft calls that one extremely early, before even
_configure_early_services()
has been calledThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is worth sweating too much just now; since this is going to a feature branch we'll have time to assess the changes in Snapcraft before setting anything in stone.