craft-application 2.6.2__tar.gz → 2.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. {craft_application-2.6.2 → craft_application-2.7.0}/PKG-INFO +3 -8
  2. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/_version.py +2 -2
  3. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/application.py +35 -28
  4. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/commands/__init__.py +5 -1
  5. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/commands/base.py +14 -0
  6. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/commands/lifecycle.py +74 -30
  7. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/errors.py +1 -1
  8. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/models/__init__.py +10 -2
  9. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/models/project.py +82 -0
  10. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/remote/utils.py +1 -1
  11. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/services/lifecycle.py +1 -1
  12. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/services/provider.py +1 -1
  13. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application.egg-info/PKG-INFO +3 -8
  14. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application.egg-info/SOURCES.txt +0 -1
  15. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application.egg-info/requires.txt +2 -7
  16. craft_application-2.7.0/docs/conf.py +42 -0
  17. {craft_application-2.6.2 → craft_application-2.7.0}/pyproject.toml +3 -8
  18. {craft_application-2.6.2 → craft_application-2.7.0}/tests/conftest.py +2 -2
  19. {craft_application-2.6.2 → craft_application-2.7.0}/tests/integration/data/build-secrets/testcraft.yaml +1 -1
  20. {craft_application-2.6.2 → craft_application-2.7.0}/tests/integration/data/invalid_projects/build-error/testcraft.yaml +1 -1
  21. {craft_application-2.6.2 → craft_application-2.7.0}/tests/integration/data/valid_projects/adoption/testcraft.yaml +1 -1
  22. {craft_application-2.6.2 → craft_application-2.7.0}/tests/integration/data/valid_projects/basic/testcraft.yaml +1 -1
  23. craft_application-2.7.0/tests/integration/data/valid_projects/environment/testcraft.yaml +18 -0
  24. {craft_application-2.6.2 → craft_application-2.7.0}/tests/integration/data/valid_projects/grammar/testcraft.yaml +1 -1
  25. {craft_application-2.6.2 → craft_application-2.7.0}/tests/integration/test_application.py +37 -2
  26. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/commands/test_base.py +8 -0
  27. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/commands/test_lifecycle.py +22 -11
  28. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/models/project_models/full_project.yaml +1 -1
  29. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/models/test_project.py +147 -59
  30. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/services/test_lifecycle.py +6 -6
  31. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/services/test_service_factory.py +1 -1
  32. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/test_application.py +51 -14
  33. craft_application-2.7.0/tests/unit/util/invalid_yaml/unhashable.yaml-invalid +2 -0
  34. {craft_application-2.6.2 → craft_application-2.7.0}/tox.ini +1 -1
  35. craft_application-2.6.2/docs/_static/css/custom.css +0 -28
  36. craft_application-2.6.2/docs/conf.py +0 -62
  37. craft_application-2.6.2/tests/integration/data/valid_projects/environment/testcraft.yaml +0 -14
  38. craft_application-2.6.2/tests/unit/util/invalid_yaml/unhashable.yaml-invalid +0 -2
  39. {craft_application-2.6.2 → craft_application-2.7.0}/.editorconfig +0 -0
  40. {craft_application-2.6.2 → craft_application-2.7.0}/.github/.jira_sync_config.yaml +0 -0
  41. {craft_application-2.6.2 → craft_application-2.7.0}/.github/ISSUE_TEMPLATE/bug.yaml +0 -0
  42. {craft_application-2.6.2 → craft_application-2.7.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  43. {craft_application-2.6.2 → craft_application-2.7.0}/.github/ISSUE_TEMPLATE/task.yaml +0 -0
  44. {craft_application-2.6.2 → craft_application-2.7.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  45. {craft_application-2.6.2 → craft_application-2.7.0}/.github/release-drafter.yaml +0 -0
  46. {craft_application-2.6.2 → craft_application-2.7.0}/.github/release-drafter.yml +0 -0
  47. {craft_application-2.6.2 → craft_application-2.7.0}/.github/renovate.json5 +0 -0
  48. {craft_application-2.6.2 → craft_application-2.7.0}/.github/workflows/cla-check.yaml +0 -0
  49. {craft_application-2.6.2 → craft_application-2.7.0}/.github/workflows/docs.yaml +0 -0
  50. {craft_application-2.6.2 → craft_application-2.7.0}/.github/workflows/release-drafter.yaml +0 -0
  51. {craft_application-2.6.2 → craft_application-2.7.0}/.github/workflows/tests.yaml +0 -0
  52. {craft_application-2.6.2 → craft_application-2.7.0}/.gitignore +0 -0
  53. {craft_application-2.6.2 → craft_application-2.7.0}/.pre-commit-config.yaml +0 -0
  54. {craft_application-2.6.2 → craft_application-2.7.0}/.readthedocs.yaml +0 -0
  55. {craft_application-2.6.2 → craft_application-2.7.0}/.yamllint.yaml +0 -0
  56. {craft_application-2.6.2 → craft_application-2.7.0}/HACKING.rst +0 -0
  57. {craft_application-2.6.2 → craft_application-2.7.0}/LICENSE +0 -0
  58. {craft_application-2.6.2 → craft_application-2.7.0}/README.rst +0 -0
  59. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/__init__.py +0 -0
  60. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/commands/other.py +0 -0
  61. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/grammar.py +0 -0
  62. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/launchpad/__init__.py +0 -0
  63. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/launchpad/errors.py +0 -0
  64. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/launchpad/launchpad.py +0 -0
  65. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/launchpad/models/__init__.py +0 -0
  66. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/launchpad/models/base.py +0 -0
  67. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/launchpad/models/build.py +0 -0
  68. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/launchpad/models/code.py +0 -0
  69. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/launchpad/models/distro.py +0 -0
  70. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/launchpad/models/project.py +0 -0
  71. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/launchpad/models/recipe.py +0 -0
  72. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/launchpad/util.py +0 -0
  73. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/misc/instance_bashrc +0 -0
  74. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/models/base.py +0 -0
  75. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/models/constraints.py +0 -0
  76. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/models/grammar.py +0 -0
  77. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/models/metadata.py +0 -0
  78. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/py.typed +0 -0
  79. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/remote/__init__.py +0 -0
  80. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/remote/errors.py +0 -0
  81. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/remote/git.py +0 -0
  82. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/remote/worktree.py +0 -0
  83. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/secrets.py +0 -0
  84. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/services/__init__.py +0 -0
  85. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/services/base.py +0 -0
  86. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/services/package.py +0 -0
  87. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/services/remotebuild.py +0 -0
  88. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/services/request.py +0 -0
  89. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/services/service_factory.py +0 -0
  90. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/util/__init__.py +0 -0
  91. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/util/callbacks.py +0 -0
  92. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/util/error_formatting.py +0 -0
  93. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/util/logging.py +0 -0
  94. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/util/paths.py +0 -0
  95. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/util/platforms.py +0 -0
  96. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/util/repositories.py +0 -0
  97. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/util/snap_config.py +0 -0
  98. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/util/string.py +0 -0
  99. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application/util/yaml.py +0 -0
  100. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application.egg-info/dependency_links.txt +0 -0
  101. {craft_application-2.6.2 → craft_application-2.7.0}/craft_application.egg-info/top_level.txt +0 -0
  102. {craft_application-2.6.2 → craft_application-2.7.0}/docs/explanation/index.rst +0 -0
  103. {craft_application-2.6.2 → craft_application-2.7.0}/docs/howto/index.rst +0 -0
  104. {craft_application-2.6.2 → craft_application-2.7.0}/docs/index.rst +0 -0
  105. {craft_application-2.6.2 → craft_application-2.7.0}/docs/reference/index.rst +0 -0
  106. {craft_application-2.6.2 → craft_application-2.7.0}/docs/tutorials/index.rst +0 -0
  107. {craft_application-2.6.2 → craft_application-2.7.0}/setup.cfg +0 -0
  108. {craft_application-2.6.2 → craft_application-2.7.0}/tests/__init__.py +0 -0
  109. {craft_application-2.6.2 → craft_application-2.7.0}/tests/integration/__init__.py +0 -0
  110. {craft_application-2.6.2 → craft_application-2.7.0}/tests/integration/conftest.py +0 -0
  111. {craft_application-2.6.2 → craft_application-2.7.0}/tests/integration/data/build-secrets/secret-source-folder/source-file.txt +0 -0
  112. {craft_application-2.6.2 → craft_application-2.7.0}/tests/integration/data/valid_projects/adoption/stderr +0 -0
  113. {craft_application-2.6.2 → craft_application-2.7.0}/tests/integration/data/valid_projects/basic/stderr +0 -0
  114. {craft_application-2.6.2 → craft_application-2.7.0}/tests/integration/data/valid_projects/environment/stderr +0 -0
  115. {craft_application-2.6.2 → craft_application-2.7.0}/tests/integration/data/valid_projects/grammar/src/on-amd64-to-amd64/hello.txt +0 -0
  116. {craft_application-2.6.2 → craft_application-2.7.0}/tests/integration/data/valid_projects/grammar/src/on-amd64-to-arm64/hello.txt +0 -0
  117. {craft_application-2.6.2 → craft_application-2.7.0}/tests/integration/data/valid_projects/grammar/stderr +0 -0
  118. {craft_application-2.6.2 → craft_application-2.7.0}/tests/integration/launchpad/__init__.py +0 -0
  119. {craft_application-2.6.2 → craft_application-2.7.0}/tests/integration/launchpad/conftest.py +0 -0
  120. {craft_application-2.6.2 → craft_application-2.7.0}/tests/integration/launchpad/test_anonymous_access.py +0 -0
  121. {craft_application-2.6.2 → craft_application-2.7.0}/tests/integration/services/__init__.py +0 -0
  122. {craft_application-2.6.2 → craft_application-2.7.0}/tests/integration/services/test_lifecycle.py +0 -0
  123. {craft_application-2.6.2 → craft_application-2.7.0}/tests/integration/services/test_provider.py +0 -0
  124. {craft_application-2.6.2 → craft_application-2.7.0}/tests/integration/services/test_remotebuild.py +0 -0
  125. {craft_application-2.6.2 → craft_application-2.7.0}/tests/integration/services/test_request.py +0 -0
  126. {craft_application-2.6.2 → craft_application-2.7.0}/tests/integration/services/test_service_factory.py +0 -0
  127. {craft_application-2.6.2 → craft_application-2.7.0}/tests/integration/test_version.py +0 -0
  128. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/__init__.py +0 -0
  129. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/commands/__init__.py +0 -0
  130. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/commands/test_other.py +0 -0
  131. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/conftest.py +0 -0
  132. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/launchpad/__init__.py +0 -0
  133. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/launchpad/conftest.py +0 -0
  134. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/launchpad/models/__init__.py +0 -0
  135. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/launchpad/models/test_base.py +0 -0
  136. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/launchpad/models/test_code.py +0 -0
  137. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/launchpad/test_launchpad.py +0 -0
  138. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/launchpad/test_util.py +0 -0
  139. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/models/__init__.py +0 -0
  140. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/models/project_models/basic_project.yaml +0 -0
  141. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/models/project_models/invalid_project.yaml +0 -0
  142. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/models/test_constraints.py +0 -0
  143. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/models/test_grammar.py +0 -0
  144. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/remote/__init__.py +0 -0
  145. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/remote/conftest.py +0 -0
  146. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/remote/test_errors.py +0 -0
  147. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/remote/test_git.py +0 -0
  148. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/remote/test_utils.py +0 -0
  149. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/remote/test_worktree.py +0 -0
  150. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/services/__init__.py +0 -0
  151. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/services/conftest.py +0 -0
  152. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/services/test_package.py +0 -0
  153. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/services/test_provider.py +0 -0
  154. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/services/test_remotebuild.py +0 -0
  155. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/services/test_repositories.py +0 -0
  156. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/services/test_request.py +0 -0
  157. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/test_errors.py +0 -0
  158. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/test_grammar.py +0 -0
  159. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/test_nothing.py +0 -0
  160. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/test_secrets.py +0 -0
  161. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/util/__init__.py +0 -0
  162. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/util/invalid_yaml/_README +0 -0
  163. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/util/invalid_yaml/duplicate_second_level.yaml-invalid +0 -0
  164. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/util/invalid_yaml/duplicate_top_level.yaml-invalid +0 -0
  165. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/util/test_error_formatting.py +0 -0
  166. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/util/test_logging.py +0 -0
  167. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/util/test_paths.py +0 -0
  168. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/util/test_snap_config.py +0 -0
  169. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/util/test_string.py +0 -0
  170. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/util/test_yaml.py +0 -0
  171. {craft_application-2.6.2 → craft_application-2.7.0}/tests/unit/util/valid_yaml/empty.yaml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: craft-application
3
- Version: 2.6.2
3
+ Version: 2.7.0
4
4
  Summary: A framework for *craft applications.
5
5
  Classifier: Development Status :: 5 - Production/Stable
6
6
  Classifier: Environment :: Console
@@ -47,17 +47,12 @@ Requires-Dist: codespell[toml]==2.2.6; extra == "lint"
47
47
  Requires-Dist: yamllint==1.35.1; extra == "lint"
48
48
  Provides-Extra: types
49
49
  Requires-Dist: mypy[reports]==1.9.0; extra == "types"
50
- Requires-Dist: pyright==1.1.358; extra == "types"
50
+ Requires-Dist: pyright==1.1.359; extra == "types"
51
51
  Requires-Dist: types-requests; extra == "types"
52
52
  Requires-Dist: types-urllib3; extra == "types"
53
53
  Provides-Extra: docs
54
- Requires-Dist: furo==2024.1.29; extra == "docs"
55
- Requires-Dist: sphinx<7.3,>=7.2.6; extra == "docs"
54
+ Requires-Dist: canonical-sphinx~=0.1; extra == "docs"
56
55
  Requires-Dist: sphinx-autobuild==2024.2.4; extra == "docs"
57
- Requires-Dist: sphinx-copybutton==0.5.2; extra == "docs"
58
- Requires-Dist: sphinx-design==0.5.0; extra == "docs"
59
- Requires-Dist: sphinx-pydantic==0.1.1; extra == "docs"
60
- Requires-Dist: sphinx-toolbox==3.5.0; extra == "docs"
61
56
  Requires-Dist: sphinx-lint==0.9.1; extra == "docs"
62
57
  Provides-Extra: apt
63
58
  Requires-Dist: python-apt>=2.4.0; sys_platform == "linux" and extra == "apt"
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '2.6.2'
16
- __version_tuple__ = version_tuple = (2, 6, 2)
15
+ __version__ = version = '2.7.0'
16
+ __version_tuple__ = version_tuple = (2, 7, 0)
@@ -280,11 +280,18 @@ class Application:
280
280
  self._full_build_plan, platform, build_for, host_arch
281
281
  )
282
282
 
283
- if platform and not build_for:
284
- all_platforms = {b.platform: b for b in self._full_build_plan}
285
- if platform not in all_platforms:
286
- raise errors.InvalidPlatformError(platform, list(all_platforms.keys()))
287
- build_for = all_platforms[platform].build_for
283
+ if not build_for:
284
+ # get the build-for arch from the platform
285
+ if platform:
286
+ all_platforms = {b.platform: b for b in self._full_build_plan}
287
+ if platform not in all_platforms:
288
+ raise errors.InvalidPlatformError(
289
+ platform, list(all_platforms.keys())
290
+ )
291
+ build_for = all_platforms[platform].build_for
292
+ # otherwise get the build-for arch from the build plan
293
+ elif self._build_plan:
294
+ build_for = self._build_plan[0].build_for
288
295
 
289
296
  # validate project grammar
290
297
  GrammarAwareProject.validate_grammar(yaml_data)
@@ -497,7 +504,7 @@ class Application:
497
504
  self._pre_run(dispatcher)
498
505
 
499
506
  managed_mode = command.run_managed(dispatcher.parsed_args())
500
- if managed_mode or command.always_load_project:
507
+ if managed_mode or command.needs_project(dispatcher.parsed_args()):
501
508
  self.services.project = self.get_project(
502
509
  platform=platform, build_for=build_for
503
510
  )
@@ -576,34 +583,29 @@ class Application:
576
583
  Performs task such as environment expansion. Note that this transforms
577
584
  ``yaml_data`` in-place.
578
585
  """
586
+ # apply application-specific transformations first because an application may
587
+ # add advanced grammar, project variables, or secrets to the yaml
588
+ yaml_data = self._extra_yaml_transform(
589
+ yaml_data, build_on=build_on, build_for=build_for
590
+ )
591
+
592
+ # At the moment there is no perfect solution for what do to do
593
+ # expand project variables or to resolve the grammar if there's
594
+ # no explicitly-provided target arch. However, we must resolve
595
+ # it with *something* otherwise we might have an invalid parts
596
+ # definition full of grammar declarations and incorrect build_for
597
+ # architectures.
598
+ build_for = build_for or build_on
599
+
579
600
  # Perform variable expansion.
580
- self._expand_environment(yaml_data)
601
+ self._expand_environment(yaml_data=yaml_data, build_for=build_for)
581
602
 
582
603
  # Handle build secrets.
583
604
  if self.app.features.build_secrets:
584
605
  self._render_secrets(yaml_data)
585
606
 
586
- # apply application-specific transformations before expanding grammar
587
- # because an application may add advanced grammar to the yaml
588
- yaml_data = self._extra_yaml_transform(
589
- yaml_data, build_on=build_on, build_for=build_for
590
- )
591
-
592
607
  # Expand grammar.
593
608
  if "parts" in yaml_data:
594
- if not build_for:
595
- # At the moment there is no perfect solution for what do to do
596
- # resolve the grammar if there's no explicitly-provided target
597
- # arch. However, we must resolve it with *something* otherwise
598
- # we might have an invalid parts definition full of grammar
599
- # declarations.
600
- if self._build_plan:
601
- # If we have a build plan use one of its items, because
602
- # it's likely contemplated on the grammar.
603
- build_for = self._build_plan[0].build_for
604
- else:
605
- # As a last resort, default to the same arch as the host.
606
- build_for = build_on
607
609
  craft_cli.emit.debug(f"Processing grammar (on {build_on} for {build_for})")
608
610
  yaml_data["parts"] = grammar.process_parts(
609
611
  parts_yaml_data=yaml_data["parts"],
@@ -613,8 +615,12 @@ class Application:
613
615
 
614
616
  return yaml_data
615
617
 
616
- def _expand_environment(self, yaml_data: dict[str, Any]) -> None:
617
- """Perform expansion of project environment variables."""
618
+ def _expand_environment(self, yaml_data: dict[str, Any], build_for: str) -> None:
619
+ """Perform expansion of project environment variables.
620
+
621
+ :param yaml_data: The project's yaml data.
622
+ :param build_for: The architecture to build for.
623
+ """
618
624
  environment_vars = self._get_project_vars(yaml_data)
619
625
  project_dirs = craft_parts.ProjectDirs(
620
626
  work_dir=self._work_dir, partitions=self._partitions
@@ -623,6 +629,7 @@ class Application:
623
629
  info = craft_parts.ProjectInfo(
624
630
  application_name=self.app.name, # not used in environment expansion
625
631
  cache_dir=pathlib.Path(), # not used in environment expansion
632
+ arch=util.convert_architecture_deb_to_platform(build_for),
626
633
  project_name=yaml_data.get("name", ""),
627
634
  project_dirs=project_dirs,
628
635
  project_vars=environment_vars,
@@ -17,13 +17,17 @@
17
17
 
18
18
  from craft_application.commands.base import AppCommand, ExtensibleCommand
19
19
  from craft_application.commands import lifecycle
20
- from craft_application.commands.lifecycle import get_lifecycle_command_group
20
+ from craft_application.commands.lifecycle import (
21
+ get_lifecycle_command_group,
22
+ LifecycleCommand,
23
+ )
21
24
  from craft_application.commands.other import get_other_command_group
22
25
 
23
26
  __all__ = [
24
27
  "AppCommand",
25
28
  "ExtensibleCommand",
26
29
  "lifecycle",
30
+ "LifecycleCommand",
27
31
  "get_lifecycle_command_group",
28
32
  "get_other_command_group",
29
33
  ]
@@ -65,6 +65,20 @@ class AppCommand(BaseCommand):
65
65
  self._app: application.AppMetadata = config["app"]
66
66
  self._services: service_factory.ServiceFactory = config["services"]
67
67
 
68
+ def needs_project(
69
+ self,
70
+ parsed_args: argparse.Namespace, # noqa: ARG002 (unused argument is for subclasses)
71
+ ) -> bool:
72
+ """Property to determine if the command needs a project loaded.
73
+
74
+ Defaults to `self.always_load_project`. Subclasses can override this property
75
+
76
+ :param parsed_args: Parsed arguments for the command.
77
+
78
+ :returns: True if the command needs a project loaded, False otherwise.
79
+ """
80
+ return self.always_load_project
81
+
68
82
  def run_managed(
69
83
  self,
70
84
  parsed_args: argparse.Namespace, # noqa: ARG002 (the unused argument is for subclasses)
@@ -32,7 +32,7 @@ if TYPE_CHECKING: # pragma: no cover
32
32
 
33
33
  def get_lifecycle_command_group() -> CommandGroup:
34
34
  """Return the lifecycle related command group."""
35
- commands: list[type[_LifecycleCommand]] = [
35
+ commands: list[type[_BaseLifecycleCommand]] = [
36
36
  CleanCommand,
37
37
  PullCommand,
38
38
  OverlayCommand,
@@ -50,30 +50,21 @@ def get_lifecycle_command_group() -> CommandGroup:
50
50
  )
51
51
 
52
52
 
53
- class _LifecycleCommand(base.ExtensibleCommand):
54
- """Lifecycle-related commands."""
53
+ class _BaseLifecycleCommand(base.ExtensibleCommand):
54
+ """Base class for lifecycle-related commands.
55
+
56
+ All lifecycle commands must know where to execute (locally or in a build
57
+ environment) but do not have to provide shell access into the environment.
58
+ """
55
59
 
56
60
  @override
57
61
  def _run(self, parsed_args: argparse.Namespace, **kwargs: Any) -> None:
58
62
  emit.trace(f"lifecycle command: {self.name!r}, arguments: {parsed_args!r}")
59
63
 
60
-
61
- class LifecyclePartsCommand(_LifecycleCommand):
62
- """A lifecycle command that uses parts."""
63
-
64
- # All lifecycle-related commands need a project to work
65
- always_load_project = True
66
-
67
64
  @override
68
65
  def _fill_parser(self, parser: argparse.ArgumentParser) -> None:
69
66
  super()._fill_parser(parser) # type: ignore[arg-type]
70
- parser.add_argument(
71
- "parts",
72
- metavar="part-name",
73
- type=str,
74
- nargs="*",
75
- help="Optional list of parts to process",
76
- )
67
+
77
68
  group = parser.add_mutually_exclusive_group()
78
69
  group.add_argument(
79
70
  "--destructive-mode",
@@ -121,8 +112,12 @@ class LifecyclePartsCommand(_LifecycleCommand):
121
112
  return True
122
113
 
123
114
 
124
- class LifecycleStepCommand(LifecyclePartsCommand):
125
- """An actual lifecycle step."""
115
+ class LifecycleCommand(_BaseLifecycleCommand):
116
+ """A command that will run the lifecycle and can shell into the environment.
117
+
118
+ LifecycleCommands do not require a part. For example 'pack' will run
119
+ the lifecycle but cannot be run on a specific part.
120
+ """
126
121
 
127
122
  @override
128
123
  def _fill_parser(self, parser: argparse.ArgumentParser) -> None:
@@ -202,10 +197,7 @@ class LifecycleStepCommand(LifecyclePartsCommand):
202
197
  shell_after = True
203
198
 
204
199
  try:
205
- self._services.lifecycle.run(
206
- step_name=step_name,
207
- part_names=parsed_args.parts,
208
- )
200
+ self._run_lifecycle(parsed_args, step_name)
209
201
  except Exception as err:
210
202
  if debug:
211
203
  emit.progress(str(err), permanent=True)
@@ -215,12 +207,48 @@ class LifecycleStepCommand(LifecyclePartsCommand):
215
207
  if shell_after:
216
208
  _launch_shell()
217
209
 
210
+ def _run_lifecycle(
211
+ self,
212
+ parsed_args: argparse.Namespace, # noqa: ARG002 (unused argument is for subclasses)
213
+ step_name: str | None = None,
214
+ ) -> None:
215
+ """Run the lifecycle."""
216
+ self._services.lifecycle.run(step_name=step_name)
217
+
218
218
  @staticmethod
219
219
  def _should_add_shell_args() -> bool:
220
220
  return True
221
221
 
222
222
 
223
- class PullCommand(LifecycleStepCommand):
223
+ class LifecyclePartsCommand(LifecycleCommand):
224
+ """A command that can run the lifecycle for a particular part."""
225
+
226
+ # All lifecycle-related commands need a project to work
227
+ always_load_project = True
228
+
229
+ @override
230
+ def _fill_parser(self, parser: argparse.ArgumentParser) -> None:
231
+ super()._fill_parser(parser) # type: ignore[arg-type]
232
+ parser.add_argument(
233
+ "parts",
234
+ metavar="part-name",
235
+ type=str,
236
+ nargs="*",
237
+ help="Optional list of parts to process",
238
+ )
239
+
240
+ @override
241
+ def _run_lifecycle(
242
+ self, parsed_args: argparse.Namespace, step_name: str | None = None
243
+ ) -> None:
244
+ """Run the lifecycle, optionally for a part or list of parts."""
245
+ self._services.lifecycle.run(
246
+ step_name=step_name,
247
+ part_names=parsed_args.parts,
248
+ )
249
+
250
+
251
+ class PullCommand(LifecyclePartsCommand):
224
252
  """Command to pull parts."""
225
253
 
226
254
  name = "pull"
@@ -234,7 +262,7 @@ class PullCommand(LifecycleStepCommand):
234
262
  )
235
263
 
236
264
 
237
- class OverlayCommand(LifecycleStepCommand):
265
+ class OverlayCommand(LifecyclePartsCommand):
238
266
  """Command to overlay parts."""
239
267
 
240
268
  name = "overlay"
@@ -247,7 +275,7 @@ class OverlayCommand(LifecycleStepCommand):
247
275
  )
248
276
 
249
277
 
250
- class BuildCommand(LifecycleStepCommand):
278
+ class BuildCommand(LifecyclePartsCommand):
251
279
  """Command to build parts."""
252
280
 
253
281
  name = "build"
@@ -260,7 +288,7 @@ class BuildCommand(LifecycleStepCommand):
260
288
  )
261
289
 
262
290
 
263
- class StageCommand(LifecycleStepCommand):
291
+ class StageCommand(LifecyclePartsCommand):
264
292
  """Command to stage parts."""
265
293
 
266
294
  name = "stage"
@@ -274,7 +302,7 @@ class StageCommand(LifecycleStepCommand):
274
302
  )
275
303
 
276
304
 
277
- class PrimeCommand(LifecycleStepCommand):
305
+ class PrimeCommand(LifecyclePartsCommand):
278
306
  """Command to prime parts."""
279
307
 
280
308
  name = "prime"
@@ -355,9 +383,10 @@ class PackCommand(PrimeCommand):
355
383
  return False
356
384
 
357
385
 
358
- class CleanCommand(LifecyclePartsCommand):
386
+ class CleanCommand(_BaseLifecycleCommand):
359
387
  """Command to remove part assets."""
360
388
 
389
+ always_load_project = True
361
390
  name = "clean"
362
391
  help_msg = "Remove a part's assets"
363
392
  overview = textwrap.dedent(
@@ -368,7 +397,22 @@ class CleanCommand(LifecyclePartsCommand):
368
397
  )
369
398
 
370
399
  @override
371
- def _run(self, parsed_args: argparse.Namespace, **kwargs: Any) -> None:
400
+ def _fill_parser(self, parser: argparse.ArgumentParser) -> None:
401
+ super()._fill_parser(parser) # type: ignore[arg-type]
402
+ parser.add_argument(
403
+ "parts",
404
+ metavar="part-name",
405
+ type=str,
406
+ nargs="*",
407
+ help="Optional list of parts to process",
408
+ )
409
+
410
+ @override
411
+ def _run(
412
+ self,
413
+ parsed_args: argparse.Namespace,
414
+ **kwargs: Any,
415
+ ) -> None:
372
416
  """Run the clean command.
373
417
 
374
418
  The project's work directory will be cleaned if:
@@ -136,7 +136,7 @@ class MultipleBuildsError(CraftError):
136
136
 
137
137
  def __init__(self) -> None:
138
138
  message = "Multiple builds match the current platform."
139
- resolution = 'Check the "--platform" and "--bulld-for" parameters.'
139
+ resolution = 'Check the "--platform" and "--build-for" parameters.'
140
140
 
141
141
  super().__init__(message=message, resolution=resolution)
142
142
 
@@ -1,6 +1,6 @@
1
1
  # This file is part of craft_application.
2
2
  #
3
- # Copyright 2023 Canonical Ltd.
3
+ # Copyright 2023-2024 Canonical Ltd.
4
4
  #
5
5
  # This program is free software: you can redistribute it and/or modify it
6
6
  # under the terms of the GNU Lesser General Public License version 3, as
@@ -28,12 +28,20 @@ from craft_application.models.grammar import (
28
28
  get_grammar_aware_part_keywords,
29
29
  )
30
30
  from craft_application.models.metadata import BaseMetadata
31
- from craft_application.models.project import BuildInfo, BuildPlanner, Project
31
+ from craft_application.models.project import (
32
+ DEVEL_BASE_INFOS,
33
+ DEVEL_BASE_WARNING,
34
+ BuildInfo,
35
+ BuildPlanner,
36
+ Project,
37
+ )
32
38
 
33
39
 
34
40
  __all__ = [
35
41
  "BaseMetadata",
36
42
  "BuildInfo",
43
+ "DEVEL_BASE_INFOS",
44
+ "DEVEL_BASE_WARNING",
37
45
  "CraftBaseConfig",
38
46
  "CraftBaseModel",
39
47
  "get_grammar_aware_part_keywords",
@@ -24,9 +24,12 @@ from typing import Any
24
24
  import craft_parts
25
25
  import craft_providers.bases
26
26
  import pydantic
27
+ from craft_cli import emit
28
+ from craft_providers.errors import BaseConfigurationError
27
29
  from pydantic import AnyUrl
28
30
  from typing_extensions import override
29
31
 
32
+ from craft_application import errors
30
33
  from craft_application.models.base import CraftBaseConfig, CraftBaseModel
31
34
  from craft_application.models.constraints import (
32
35
  MESSAGE_INVALID_NAME,
@@ -39,6 +42,33 @@ from craft_application.models.constraints import (
39
42
  )
40
43
 
41
44
 
45
+ @dataclasses.dataclass
46
+ class DevelBaseInfo:
47
+ """Devel base information for an OS."""
48
+
49
+ current_devel_base: craft_providers.bases.BaseAlias
50
+ """The base that the 'devel' alias currently points to."""
51
+
52
+ devel_base: craft_providers.bases.BaseAlias
53
+ """The devel base."""
54
+
55
+
56
+ # A list of DevelBaseInfo objects that define an OS's current devel base and devel base.
57
+ DEVEL_BASE_INFOS = [
58
+ DevelBaseInfo(
59
+ # TODO: current_devel_base should point to 24.10, which is not available yet
60
+ current_devel_base=craft_providers.bases.ubuntu.BuilddBaseAlias.DEVEL,
61
+ devel_base=craft_providers.bases.ubuntu.BuilddBaseAlias.DEVEL,
62
+ ),
63
+ ]
64
+
65
+ DEVEL_BASE_WARNING = (
66
+ "The development build-base should only be used for testing purposes, "
67
+ "as its contents are bound to change with the opening of new Ubuntu releases, "
68
+ "suddenly and without warning."
69
+ )
70
+
71
+
42
72
  @dataclasses.dataclass
43
73
  class BuildInfo:
44
74
  """Platform build information."""
@@ -115,6 +145,58 @@ class Project(CraftBaseModel):
115
145
  return self.base
116
146
  raise RuntimeError("Could not determine effective base")
117
147
 
148
+ @classmethod
149
+ def _providers_base(cls, base: str) -> craft_providers.bases.BaseAlias | None:
150
+ """Get a BaseAlias from the Project base.
151
+
152
+ The default naming convention for a base is 'name@channel'. This method
153
+ should be overridden for applications with a different naming convention.
154
+
155
+ :param base: The base name.
156
+
157
+ :returns: The BaseAlias for the base or None if the base does not map to
158
+ a BaseAlias.
159
+
160
+ :raises CraftValidationError: If the project's base cannot be determined.
161
+ """
162
+ try:
163
+ name, channel = base.split("@")
164
+ return craft_providers.bases.get_base_alias((name, channel))
165
+ except (ValueError, BaseConfigurationError) as err:
166
+ raise errors.CraftValidationError(f"Unknown base {base!r}") from err
167
+
168
+ @pydantic.root_validator( # pyright: ignore[reportUnknownMemberType,reportUntypedFunctionDecorator]
169
+ pre=False
170
+ )
171
+ def _validate_devel(cls, values: dict[str, Any]) -> dict[str, Any]:
172
+ """Validate the build-base is 'devel' for the current devel base."""
173
+ base = values.get("base")
174
+ # if there is no base, do not validate the build-base
175
+ if not base:
176
+ return values
177
+
178
+ base_alias = cls._providers_base(base)
179
+
180
+ # if the base does not map to a base alias, do not validate the build-base
181
+ if not base_alias:
182
+ return values
183
+
184
+ build_base = values.get("build_base") or base
185
+ build_base_alias = cls._providers_base(build_base)
186
+
187
+ # warn if a devel build-base is being used, error if a devel build-base is not
188
+ # used for a devel base
189
+ for devel_base_info in DEVEL_BASE_INFOS:
190
+ if base_alias == devel_base_info.current_devel_base:
191
+ if build_base_alias == devel_base_info.devel_base:
192
+ emit.message(DEVEL_BASE_WARNING)
193
+ else:
194
+ raise errors.CraftValidationError(
195
+ f"A development build-base must be used when base is {base!r}"
196
+ )
197
+
198
+ return values
199
+
118
200
  @override
119
201
  @classmethod
120
202
  def transform_pydantic_error(cls, error: pydantic.ValidationError) -> None:
@@ -85,7 +85,7 @@ def _compute_hash(directory: Path) -> str:
85
85
  "a directory."
86
86
  )
87
87
 
88
- files = sorted([file for file in Path().glob("**/*") if file.is_file()])
88
+ files = sorted(file for file in directory.glob("**/*") if file.is_file())
89
89
  hashes: list[str] = []
90
90
 
91
91
  for file_path in files:
@@ -263,7 +263,7 @@ class LifecycleService(base.ProjectService):
263
263
  raise RuntimeError(f"Parts processing internal error: {err}") from err
264
264
  except OSError as err:
265
265
  raise errors.PartsLifecycleError.from_os_error(err) from err
266
- except Exception as err: # noqa: BLE001 - Converting general error.
266
+ except Exception as err:
267
267
  raise errors.PartsLifecycleError(f"Unknown error: {str(err)}") from err
268
268
 
269
269
  def post_prime(self, step_info: StepInfo) -> bool:
@@ -105,7 +105,7 @@ class ProviderService(base.ProjectService):
105
105
  ) -> Generator[craft_providers.Executor, None, None]:
106
106
  """Context manager for getting a provider instance.
107
107
 
108
- :param base_name: A craft_providers capable base name (tuple of name, version)
108
+ :param build_info: Build information for the instance.
109
109
  :param work_dir: Local path to mount inside the provider instance.
110
110
  :param allow_unstable: Whether to allow the use of unstable images.
111
111
  :returns: a context manager of the provider instance.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: craft-application
3
- Version: 2.6.2
3
+ Version: 2.7.0
4
4
  Summary: A framework for *craft applications.
5
5
  Classifier: Development Status :: 5 - Production/Stable
6
6
  Classifier: Environment :: Console
@@ -47,17 +47,12 @@ Requires-Dist: codespell[toml]==2.2.6; extra == "lint"
47
47
  Requires-Dist: yamllint==1.35.1; extra == "lint"
48
48
  Provides-Extra: types
49
49
  Requires-Dist: mypy[reports]==1.9.0; extra == "types"
50
- Requires-Dist: pyright==1.1.358; extra == "types"
50
+ Requires-Dist: pyright==1.1.359; extra == "types"
51
51
  Requires-Dist: types-requests; extra == "types"
52
52
  Requires-Dist: types-urllib3; extra == "types"
53
53
  Provides-Extra: docs
54
- Requires-Dist: furo==2024.1.29; extra == "docs"
55
- Requires-Dist: sphinx<7.3,>=7.2.6; extra == "docs"
54
+ Requires-Dist: canonical-sphinx~=0.1; extra == "docs"
56
55
  Requires-Dist: sphinx-autobuild==2024.2.4; extra == "docs"
57
- Requires-Dist: sphinx-copybutton==0.5.2; extra == "docs"
58
- Requires-Dist: sphinx-design==0.5.0; extra == "docs"
59
- Requires-Dist: sphinx-pydantic==0.1.1; extra == "docs"
60
- Requires-Dist: sphinx-toolbox==3.5.0; extra == "docs"
61
56
  Requires-Dist: sphinx-lint==0.9.1; extra == "docs"
62
57
  Provides-Extra: apt
63
58
  Requires-Dist: python-apt>=2.4.0; sys_platform == "linux" and extra == "apt"
@@ -79,7 +79,6 @@ craft_application/util/string.py
79
79
  craft_application/util/yaml.py
80
80
  docs/conf.py
81
81
  docs/index.rst
82
- docs/_static/css/custom.css
83
82
  docs/explanation/index.rst
84
83
  docs/howto/index.rst
85
84
  docs/reference/index.rst
@@ -30,13 +30,8 @@ responses~=0.25.0
30
30
  craft-application[remote]
31
31
 
32
32
  [docs]
33
- furo==2024.1.29
34
- sphinx<7.3,>=7.2.6
33
+ canonical-sphinx~=0.1
35
34
  sphinx-autobuild==2024.2.4
36
- sphinx-copybutton==0.5.2
37
- sphinx-design==0.5.0
38
- sphinx-pydantic==0.1.1
39
- sphinx-toolbox==3.5.0
40
35
  sphinx-lint==0.9.1
41
36
 
42
37
  [lint]
@@ -49,6 +44,6 @@ launchpadlib>=1.10.16
49
44
 
50
45
  [types]
51
46
  mypy[reports]==1.9.0
52
- pyright==1.1.358
47
+ pyright==1.1.359
53
48
  types-requests
54
49
  types-urllib3
@@ -0,0 +1,42 @@
1
+ # Copyright 2023-2024 Canonical Ltd.
2
+ #
3
+ # This program is free software; you can redistribute it and/or
4
+ # modify it under the terms of the GNU Lesser General Public
5
+ # License version 3 as published by the Free Software Foundation.
6
+ #
7
+ # This program is distributed in the hope that it will be useful,
8
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
10
+ # Lesser General Public License for more details.
11
+ #
12
+ # You should have received a copy of the GNU Lesser General Public License
13
+ # along with this program; if not, write to the Free Software Foundation,
14
+ # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
15
+ #
16
+
17
+ import datetime
18
+
19
+ project = "Craft Application"
20
+ author = "Canonical Group Ltd"
21
+
22
+ copyright = "2023-%s, %s" % (datetime.date.today().year, author)
23
+
24
+ # region Configuration for canonical-sphinx
25
+ ogp_site_url = "https://canonical-craft-application.readthedocs-hosted.com/"
26
+ ogp_site_name = project
27
+
28
+ html_context = {
29
+ "product_page": "github.com/canonical/craft-application",
30
+ "github_url": "https://github.com/canonical/craft-application",
31
+ }
32
+
33
+ extensions = [
34
+ "canonical_sphinx",
35
+ ]
36
+ # endregion
37
+
38
+ # region Options for extensions
39
+ # Github config
40
+ github_username = "canonical"
41
+ github_repository = "craft-application"
42
+ # endregion