craft-application 2.2.0__tar.gz → 2.4.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 (167) hide show
  1. {craft-application-2.2.0 → craft-application-2.4.0}/PKG-INFO +4 -4
  2. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/_version.py +2 -2
  3. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/application.py +32 -8
  4. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/launchpad/models/project.py +2 -1
  5. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/services/lifecycle.py +3 -0
  6. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/services/provider.py +1 -2
  7. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/services/remotebuild.py +38 -9
  8. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application.egg-info/PKG-INFO +4 -4
  9. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application.egg-info/SOURCES.txt +1 -0
  10. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application.egg-info/requires.txt +3 -3
  11. {craft-application-2.2.0 → craft-application-2.4.0}/pyproject.toml +3 -3
  12. {craft-application-2.2.0 → craft-application-2.4.0}/tests/conftest.py +25 -1
  13. {craft-application-2.2.0 → craft-application-2.4.0}/tests/integration/conftest.py +11 -1
  14. craft-application-2.4.0/tests/integration/services/test_remotebuild.py +47 -0
  15. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/services/test_lifecycle.py +20 -0
  16. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/services/test_provider.py +0 -4
  17. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/services/test_remotebuild.py +59 -1
  18. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/test_application.py +64 -1
  19. {craft-application-2.2.0 → craft-application-2.4.0}/.editorconfig +0 -0
  20. {craft-application-2.2.0 → craft-application-2.4.0}/.github/.jira_sync_config.yaml +0 -0
  21. {craft-application-2.2.0 → craft-application-2.4.0}/.github/ISSUE_TEMPLATE/bug.yaml +0 -0
  22. {craft-application-2.2.0 → craft-application-2.4.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  23. {craft-application-2.2.0 → craft-application-2.4.0}/.github/ISSUE_TEMPLATE/task.yaml +0 -0
  24. {craft-application-2.2.0 → craft-application-2.4.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  25. {craft-application-2.2.0 → craft-application-2.4.0}/.github/release-drafter.yaml +0 -0
  26. {craft-application-2.2.0 → craft-application-2.4.0}/.github/release-drafter.yml +0 -0
  27. {craft-application-2.2.0 → craft-application-2.4.0}/.github/renovate.json5 +0 -0
  28. {craft-application-2.2.0 → craft-application-2.4.0}/.github/workflows/cla-check.yaml +0 -0
  29. {craft-application-2.2.0 → craft-application-2.4.0}/.github/workflows/docs.yaml +0 -0
  30. {craft-application-2.2.0 → craft-application-2.4.0}/.github/workflows/release-drafter.yaml +0 -0
  31. {craft-application-2.2.0 → craft-application-2.4.0}/.github/workflows/tests.yaml +0 -0
  32. {craft-application-2.2.0 → craft-application-2.4.0}/.gitignore +0 -0
  33. {craft-application-2.2.0 → craft-application-2.4.0}/.pre-commit-config.yaml +0 -0
  34. {craft-application-2.2.0 → craft-application-2.4.0}/.readthedocs.yaml +0 -0
  35. {craft-application-2.2.0 → craft-application-2.4.0}/.yamllint.yaml +0 -0
  36. {craft-application-2.2.0 → craft-application-2.4.0}/HACKING.rst +0 -0
  37. {craft-application-2.2.0 → craft-application-2.4.0}/LICENSE +0 -0
  38. {craft-application-2.2.0 → craft-application-2.4.0}/README.rst +0 -0
  39. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/__init__.py +0 -0
  40. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/commands/__init__.py +0 -0
  41. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/commands/base.py +0 -0
  42. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/commands/lifecycle.py +0 -0
  43. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/commands/other.py +0 -0
  44. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/errors.py +0 -0
  45. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/grammar.py +0 -0
  46. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/launchpad/__init__.py +0 -0
  47. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/launchpad/errors.py +0 -0
  48. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/launchpad/launchpad.py +0 -0
  49. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/launchpad/models/__init__.py +0 -0
  50. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/launchpad/models/base.py +0 -0
  51. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/launchpad/models/build.py +0 -0
  52. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/launchpad/models/code.py +0 -0
  53. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/launchpad/models/distro.py +0 -0
  54. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/launchpad/models/recipe.py +0 -0
  55. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/launchpad/util.py +0 -0
  56. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/misc/instance_bashrc +0 -0
  57. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/models/__init__.py +0 -0
  58. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/models/base.py +0 -0
  59. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/models/constraints.py +0 -0
  60. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/models/grammar.py +0 -0
  61. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/models/metadata.py +0 -0
  62. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/models/project.py +0 -0
  63. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/py.typed +0 -0
  64. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/remote/__init__.py +0 -0
  65. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/remote/errors.py +0 -0
  66. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/remote/git.py +0 -0
  67. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/remote/utils.py +0 -0
  68. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/remote/worktree.py +0 -0
  69. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/secrets.py +0 -0
  70. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/services/__init__.py +0 -0
  71. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/services/base.py +0 -0
  72. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/services/package.py +0 -0
  73. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/services/request.py +0 -0
  74. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/services/service_factory.py +0 -0
  75. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/util/__init__.py +0 -0
  76. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/util/callbacks.py +0 -0
  77. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/util/error_formatting.py +0 -0
  78. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/util/logging.py +0 -0
  79. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/util/paths.py +0 -0
  80. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/util/platforms.py +0 -0
  81. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/util/repositories.py +0 -0
  82. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/util/snap_config.py +0 -0
  83. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/util/string.py +0 -0
  84. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application/util/yaml.py +0 -0
  85. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application.egg-info/dependency_links.txt +0 -0
  86. {craft-application-2.2.0 → craft-application-2.4.0}/craft_application.egg-info/top_level.txt +0 -0
  87. {craft-application-2.2.0 → craft-application-2.4.0}/docs/_static/css/custom.css +0 -0
  88. {craft-application-2.2.0 → craft-application-2.4.0}/docs/conf.py +0 -0
  89. {craft-application-2.2.0 → craft-application-2.4.0}/docs/explanation/index.rst +0 -0
  90. {craft-application-2.2.0 → craft-application-2.4.0}/docs/howto/index.rst +0 -0
  91. {craft-application-2.2.0 → craft-application-2.4.0}/docs/index.rst +0 -0
  92. {craft-application-2.2.0 → craft-application-2.4.0}/docs/reference/index.rst +0 -0
  93. {craft-application-2.2.0 → craft-application-2.4.0}/docs/tutorials/index.rst +0 -0
  94. {craft-application-2.2.0 → craft-application-2.4.0}/setup.cfg +0 -0
  95. {craft-application-2.2.0 → craft-application-2.4.0}/tests/__init__.py +0 -0
  96. {craft-application-2.2.0 → craft-application-2.4.0}/tests/integration/__init__.py +0 -0
  97. {craft-application-2.2.0 → craft-application-2.4.0}/tests/integration/data/build-secrets/secret-source-folder/source-file.txt +0 -0
  98. {craft-application-2.2.0 → craft-application-2.4.0}/tests/integration/data/build-secrets/testcraft.yaml +0 -0
  99. {craft-application-2.2.0 → craft-application-2.4.0}/tests/integration/data/invalid_projects/build-error/testcraft.yaml +0 -0
  100. {craft-application-2.2.0 → craft-application-2.4.0}/tests/integration/data/valid_projects/adoption/stderr +0 -0
  101. {craft-application-2.2.0 → craft-application-2.4.0}/tests/integration/data/valid_projects/adoption/testcraft.yaml +0 -0
  102. {craft-application-2.2.0 → craft-application-2.4.0}/tests/integration/data/valid_projects/basic/stderr +0 -0
  103. {craft-application-2.2.0 → craft-application-2.4.0}/tests/integration/data/valid_projects/basic/testcraft.yaml +0 -0
  104. {craft-application-2.2.0 → craft-application-2.4.0}/tests/integration/data/valid_projects/environment/stderr +0 -0
  105. {craft-application-2.2.0 → craft-application-2.4.0}/tests/integration/data/valid_projects/environment/testcraft.yaml +0 -0
  106. {craft-application-2.2.0 → craft-application-2.4.0}/tests/integration/data/valid_projects/grammar/src/on-amd64-to-amd64/hello.txt +0 -0
  107. {craft-application-2.2.0 → craft-application-2.4.0}/tests/integration/data/valid_projects/grammar/src/on-amd64-to-arm64/hello.txt +0 -0
  108. {craft-application-2.2.0 → craft-application-2.4.0}/tests/integration/data/valid_projects/grammar/stderr +0 -0
  109. {craft-application-2.2.0 → craft-application-2.4.0}/tests/integration/data/valid_projects/grammar/testcraft.yaml +0 -0
  110. {craft-application-2.2.0 → craft-application-2.4.0}/tests/integration/launchpad/__init__.py +0 -0
  111. {craft-application-2.2.0 → craft-application-2.4.0}/tests/integration/launchpad/conftest.py +0 -0
  112. {craft-application-2.2.0 → craft-application-2.4.0}/tests/integration/launchpad/test_anonymous_access.py +0 -0
  113. {craft-application-2.2.0 → craft-application-2.4.0}/tests/integration/services/__init__.py +0 -0
  114. {craft-application-2.2.0 → craft-application-2.4.0}/tests/integration/services/test_lifecycle.py +0 -0
  115. {craft-application-2.2.0 → craft-application-2.4.0}/tests/integration/services/test_provider.py +0 -0
  116. {craft-application-2.2.0 → craft-application-2.4.0}/tests/integration/services/test_request.py +0 -0
  117. {craft-application-2.2.0 → craft-application-2.4.0}/tests/integration/services/test_service_factory.py +0 -0
  118. {craft-application-2.2.0 → craft-application-2.4.0}/tests/integration/test_application.py +0 -0
  119. {craft-application-2.2.0 → craft-application-2.4.0}/tests/integration/test_version.py +0 -0
  120. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/__init__.py +0 -0
  121. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/commands/__init__.py +0 -0
  122. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/commands/test_base.py +0 -0
  123. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/commands/test_lifecycle.py +0 -0
  124. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/commands/test_other.py +0 -0
  125. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/conftest.py +0 -0
  126. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/launchpad/__init__.py +0 -0
  127. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/launchpad/conftest.py +0 -0
  128. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/launchpad/models/__init__.py +0 -0
  129. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/launchpad/models/test_base.py +0 -0
  130. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/launchpad/models/test_code.py +0 -0
  131. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/launchpad/test_launchpad.py +0 -0
  132. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/launchpad/test_util.py +0 -0
  133. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/models/__init__.py +0 -0
  134. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/models/project_models/basic_project.yaml +0 -0
  135. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/models/project_models/full_project.yaml +0 -0
  136. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/models/project_models/invalid_project.yaml +0 -0
  137. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/models/test_constraints.py +0 -0
  138. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/models/test_project.py +0 -0
  139. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/remote/__init__.py +0 -0
  140. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/remote/conftest.py +0 -0
  141. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/remote/test_errors.py +0 -0
  142. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/remote/test_git.py +0 -0
  143. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/remote/test_utils.py +0 -0
  144. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/remote/test_worktree.py +0 -0
  145. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/services/__init__.py +0 -0
  146. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/services/conftest.py +0 -0
  147. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/services/test_package.py +0 -0
  148. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/services/test_repositories.py +0 -0
  149. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/services/test_request.py +0 -0
  150. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/services/test_service_factory.py +0 -0
  151. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/test_errors.py +0 -0
  152. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/test_grammar.py +0 -0
  153. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/test_nothing.py +0 -0
  154. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/test_secrets.py +0 -0
  155. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/util/__init__.py +0 -0
  156. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/util/invalid_yaml/_README +0 -0
  157. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/util/invalid_yaml/duplicate_second_level.yaml-invalid +0 -0
  158. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/util/invalid_yaml/duplicate_top_level.yaml-invalid +0 -0
  159. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/util/invalid_yaml/unhashable.yaml-invalid +0 -0
  160. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/util/test_error_formatting.py +0 -0
  161. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/util/test_logging.py +0 -0
  162. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/util/test_paths.py +0 -0
  163. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/util/test_snap_config.py +0 -0
  164. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/util/test_string.py +0 -0
  165. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/util/test_yaml.py +0 -0
  166. {craft-application-2.2.0 → craft-application-2.4.0}/tests/unit/util/valid_yaml/empty.yaml +0 -0
  167. {craft-application-2.2.0 → craft-application-2.4.0}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: craft-application
3
- Version: 2.2.0
3
+ Version: 2.4.0
4
4
  Summary: A framework for *craft applications.
5
5
  Classifier: Development Status :: 5 - Production/Stable
6
6
  Classifier: Environment :: Console
@@ -30,13 +30,13 @@ Requires-Dist: typing_extensions>=4.4.0
30
30
  Provides-Extra: remote
31
31
  Requires-Dist: launchpadlib>=1.10.16; extra == "remote"
32
32
  Provides-Extra: dev
33
- Requires-Dist: coverage[toml]==7.4.3; extra == "dev"
33
+ Requires-Dist: coverage[toml]==7.4.4; extra == "dev"
34
34
  Requires-Dist: hypothesis>=6.0; extra == "dev"
35
35
  Requires-Dist: pyfakefs~=5.3; extra == "dev"
36
36
  Requires-Dist: pytest==8.1.1; extra == "dev"
37
37
  Requires-Dist: pytest-check==2.3.1; extra == "dev"
38
38
  Requires-Dist: pytest-cov==5.0.0; extra == "dev"
39
- Requires-Dist: pytest-mock==3.12.0; extra == "dev"
39
+ Requires-Dist: pytest-mock==3.14.0; extra == "dev"
40
40
  Requires-Dist: pytest-rerunfailures==13.0; extra == "dev"
41
41
  Requires-Dist: pytest-time>=0.3.1; extra == "dev"
42
42
  Requires-Dist: responses~=0.25.0; extra == "dev"
@@ -47,7 +47,7 @@ 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.353; extra == "types"
50
+ Requires-Dist: pyright==1.1.356; extra == "types"
51
51
  Requires-Dist: types-requests; extra == "types"
52
52
  Requires-Dist: types-urllib3; extra == "types"
53
53
  Provides-Extra: docs
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '2.2.0'
16
- __version_tuple__ = version_tuple = (2, 2, 0)
15
+ __version__ = version = '2.4.0'
16
+ __version_tuple__ = version_tuple = (2, 4, 0)
@@ -130,6 +130,7 @@ class Application:
130
130
  # When build_secrets are enabled, this contains the secret info to pass to
131
131
  # managed instances.
132
132
  self._secrets: secrets.BuildSecrets | None = None
133
+ self._partitions: list[str] | None = None
133
134
  # Cached project object, allows only the first time we load the project
134
135
  # to specify things like the project directory.
135
136
  # This is set as a private attribute in order to discourage real application
@@ -219,6 +220,7 @@ class Application:
219
220
  cache_dir=self.cache_dir,
220
221
  work_dir=self._work_dir,
221
222
  build_plan=self._build_plan,
223
+ partitions=self._partitions,
222
224
  )
223
225
  self.services.set_kwargs(
224
226
  "provider",
@@ -288,6 +290,9 @@ class Application:
288
290
  GrammarAwareProject.validate_grammar(yaml_data)
289
291
 
290
292
  build_on = host_arch
293
+
294
+ # Setup partitions, some projects require the yaml data, most will not
295
+ self._partitions = self._setup_partitions(yaml_data)
291
296
  yaml_data = self._transform_project_yaml(yaml_data, build_on, build_for)
292
297
  self.__project = self.app.ProjectClass.from_yaml_data(yaml_data, project_path)
293
298
 
@@ -344,14 +349,19 @@ class Application:
344
349
  with self.services.provider.instance(
345
350
  build_info, work_dir=self._work_dir
346
351
  ) as instance:
352
+ cmd = [self.app.name, *sys.argv[1:]]
353
+ craft_cli.emit.debug(
354
+ f"Executing {cmd} in instance location {instance_path} with {extra_args}."
355
+ )
347
356
  try:
348
- # Pyright doesn't fully understand craft_providers's CompletedProcess.
349
- instance.execute_run( # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
350
- [self.app.name, *sys.argv[1:]],
351
- cwd=instance_path,
352
- check=True,
353
- **extra_args,
354
- )
357
+ with craft_cli.emit.pause():
358
+ # Pyright doesn't fully understand craft_providers's CompletedProcess.
359
+ instance.execute_run( # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
360
+ cmd,
361
+ cwd=instance_path,
362
+ check=True,
363
+ **extra_args,
364
+ )
355
365
  except subprocess.CalledProcessError as exc:
356
366
  raise craft_providers.ProviderError(
357
367
  f"Failed to execute {self.app.name} in instance."
@@ -603,18 +613,32 @@ class Application:
603
613
  def _expand_environment(self, yaml_data: dict[str, Any]) -> None:
604
614
  """Perform expansion of project environment variables."""
605
615
  environment_vars = self._get_project_vars(yaml_data)
616
+ project_dirs = craft_parts.ProjectDirs(
617
+ work_dir=self._work_dir, partitions=self._partitions
618
+ )
619
+
606
620
  info = craft_parts.ProjectInfo(
607
621
  application_name=self.app.name, # not used in environment expansion
608
622
  cache_dir=pathlib.Path(), # not used in environment expansion
609
623
  project_name=yaml_data.get("name", ""),
610
- project_dirs=craft_parts.ProjectDirs(work_dir=self._work_dir),
624
+ project_dirs=project_dirs,
611
625
  project_vars=environment_vars,
626
+ partitions=self._partitions,
612
627
  )
613
628
 
614
629
  self._set_global_environment(info)
615
630
 
616
631
  craft_parts.expand_environment(yaml_data, info=info)
617
632
 
633
+ def _setup_partitions(self, yaml_data: dict[str, Any]) -> list[str] | None:
634
+ """Return partitions to be used.
635
+
636
+ When returning you will also need to ensure that the feature is enabled
637
+ on Application instantiation craft_parts.Features(partitions_enabled=True)
638
+ """
639
+ _ = yaml_data
640
+ return None
641
+
618
642
  def _get_project_vars(self, yaml_data: dict[str, Any]) -> dict[str, str]:
619
643
  """Return a dict with project variables to be expanded."""
620
644
  pvars: dict[str, str] = {}
@@ -37,7 +37,7 @@ import launchpadlib.errors # type: ignore[import-untyped]
37
37
  from typing_extensions import Self, Any
38
38
  from typing import TYPE_CHECKING
39
39
 
40
- from ..models.base import LaunchpadObject
40
+ from ..models.base import InformationType, LaunchpadObject
41
41
  from .. import errors
42
42
 
43
43
  if TYPE_CHECKING:
@@ -64,6 +64,7 @@ class Project(LaunchpadObject):
64
64
  display_name: str
65
65
  summary: str
66
66
  description: str
67
+ information_type: InformationType
67
68
 
68
69
  @classmethod
69
70
  def new( # pyright: ignore[reportIncompatibleMethodOverride]
@@ -134,12 +134,14 @@ class LifecycleService(base.ProjectService):
134
134
  work_dir: Path | str,
135
135
  cache_dir: Path | str,
136
136
  build_plan: list[models.BuildInfo],
137
+ partitions: list[str] | None = None,
137
138
  **lifecycle_kwargs: Any, # noqa: ANN401 - eventually used in an Any
138
139
  ) -> None:
139
140
  super().__init__(app, services, project=project)
140
141
  self._work_dir = work_dir
141
142
  self._cache_dir = cache_dir
142
143
  self._build_plan = build_plan
144
+ self._partitions = partitions
143
145
  self._manager_kwargs = lifecycle_kwargs
144
146
  self._lcm: LifecycleManager = None # type: ignore[assignment]
145
147
 
@@ -190,6 +192,7 @@ class LifecycleService(base.ProjectService):
190
192
  project_vars_part_name=self._project.adopt_info,
191
193
  project_vars=self._project_vars,
192
194
  track_stage_packages=True,
195
+ partitions=self._partitions,
193
196
  **self._manager_kwargs,
194
197
  )
195
198
  except PartsError as err:
@@ -133,8 +133,7 @@ class ProviderService(base.ProjectService):
133
133
  emit.debug("Instance launched and working directory mounted")
134
134
  self._setup_instance_bashrc(instance)
135
135
  try:
136
- with emit.pause():
137
- yield instance
136
+ yield instance
138
137
  finally:
139
138
  self._capture_logs_from_instance(instance)
140
139
 
@@ -62,6 +62,7 @@ class RemoteBuildService(base.AppService):
62
62
  self._repository: launchpad.models.GitRepository = None # type: ignore[assignment]
63
63
  self._recipe: launchpad.models.recipe.BaseRecipe = None # type: ignore[assignment]
64
64
  self._builds: Collection[launchpad.models.Build] = []
65
+ self._project_name: str | None = None
65
66
 
66
67
  def setup(self) -> None:
67
68
  """Set up the remote builder."""
@@ -69,6 +70,35 @@ class RemoteBuildService(base.AppService):
69
70
 
70
71
  # region Public API
71
72
  # Commands will call a subset of these methods, generally in order.
73
+ def set_project(self, name: str) -> None:
74
+ """Set the name of the project to use."""
75
+ if self._is_setup:
76
+ raise RuntimeError(
77
+ "Project name may only be set before starting or resuming builds."
78
+ )
79
+ try:
80
+ self._lp_project = self.lp.get_project(name)
81
+ except launchpad.errors.NotFoundError:
82
+ raise errors.CraftError(
83
+ message=f"Could not find project on Launchpad: {name}",
84
+ resolution="Ensure the project exists and that you have access to it.",
85
+ retcode=77, # EX_NOPERM from sysexits.h
86
+ reportable=False,
87
+ logpath_report=False,
88
+ )
89
+ self._project_name = name
90
+
91
+ def is_project_private(self) -> bool:
92
+ """Check whether the named project is private."""
93
+ if not self._lp_project:
94
+ raise RuntimeError(
95
+ "Cannot check if the project is private before setting its name."
96
+ )
97
+ return self._lp_project.information_type not in (
98
+ launchpad.models.InformationType.PUBLIC,
99
+ launchpad.models.InformationType.PUBLIC_SECURITY,
100
+ )
101
+
72
102
  def set_timeout(self, seconds_in_future: int) -> None:
73
103
  """Set the deadline to a certain number of seconds in the future."""
74
104
  self._deadline = time.monotonic_ns() + (seconds_in_future * 10**9)
@@ -207,17 +237,16 @@ class RemoteBuildService(base.AppService):
207
237
 
208
238
  def _ensure_project(self) -> launchpad.models.Project:
209
239
  """Ensure that the user's remote build project exists."""
240
+ if self._lp_project:
241
+ return self._lp_project
210
242
  try:
211
- return launchpad.models.Project.get(
212
- self.lp, f"{self.lp.username}-craft-remote-build"
213
- )
243
+ return self.lp.get_project(f"{self.lp.username}-craft-remote-build")
214
244
  except launchpad.errors.NotFoundError:
215
- return launchpad.models.Project.new(
216
- self.lp,
217
- f"*craft remote builds for {self.lp.username}",
218
- f"{self.lp.username}-craft-remote-build",
219
- f"{self.lp.username} remote builds",
220
- f"Automatically-generated project for housing {self.lp.username}'s remote builds in snapcraft, charmcraft, etc.",
245
+ return self.lp.new_project(
246
+ name=f"{self.lp.username}-craft-remote-build",
247
+ title=f"*craft remote builds for {self.lp.username}",
248
+ display_name=f"{self.lp.username} remote builds",
249
+ summary=f"Automatically-generated project for housing {self.lp.username}'s remote builds in snapcraft, charmcraft, etc.",
221
250
  )
222
251
 
223
252
  def _new_repository(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: craft-application
3
- Version: 2.2.0
3
+ Version: 2.4.0
4
4
  Summary: A framework for *craft applications.
5
5
  Classifier: Development Status :: 5 - Production/Stable
6
6
  Classifier: Environment :: Console
@@ -30,13 +30,13 @@ Requires-Dist: typing_extensions>=4.4.0
30
30
  Provides-Extra: remote
31
31
  Requires-Dist: launchpadlib>=1.10.16; extra == "remote"
32
32
  Provides-Extra: dev
33
- Requires-Dist: coverage[toml]==7.4.3; extra == "dev"
33
+ Requires-Dist: coverage[toml]==7.4.4; extra == "dev"
34
34
  Requires-Dist: hypothesis>=6.0; extra == "dev"
35
35
  Requires-Dist: pyfakefs~=5.3; extra == "dev"
36
36
  Requires-Dist: pytest==8.1.1; extra == "dev"
37
37
  Requires-Dist: pytest-check==2.3.1; extra == "dev"
38
38
  Requires-Dist: pytest-cov==5.0.0; extra == "dev"
39
- Requires-Dist: pytest-mock==3.12.0; extra == "dev"
39
+ Requires-Dist: pytest-mock==3.14.0; extra == "dev"
40
40
  Requires-Dist: pytest-rerunfailures==13.0; extra == "dev"
41
41
  Requires-Dist: pytest-time>=0.3.1; extra == "dev"
42
42
  Requires-Dist: responses~=0.25.0; extra == "dev"
@@ -47,7 +47,7 @@ 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.353; extra == "types"
50
+ Requires-Dist: pyright==1.1.356; extra == "types"
51
51
  Requires-Dist: types-requests; extra == "types"
52
52
  Requires-Dist: types-urllib3; extra == "types"
53
53
  Provides-Extra: docs
@@ -109,6 +109,7 @@ tests/integration/launchpad/test_anonymous_access.py
109
109
  tests/integration/services/__init__.py
110
110
  tests/integration/services/test_lifecycle.py
111
111
  tests/integration/services/test_provider.py
112
+ tests/integration/services/test_remotebuild.py
112
113
  tests/integration/services/test_request.py
113
114
  tests/integration/services/test_service_factory.py
114
115
  tests/unit/__init__.py
@@ -17,13 +17,13 @@ typing_extensions>=4.4.0
17
17
  python-apt>=2.4.0
18
18
 
19
19
  [dev]
20
- coverage[toml]==7.4.3
20
+ coverage[toml]==7.4.4
21
21
  hypothesis>=6.0
22
22
  pyfakefs~=5.3
23
23
  pytest==8.1.1
24
24
  pytest-check==2.3.1
25
25
  pytest-cov==5.0.0
26
- pytest-mock==3.12.0
26
+ pytest-mock==3.14.0
27
27
  pytest-rerunfailures==13.0
28
28
  pytest-time>=0.3.1
29
29
  responses~=0.25.0
@@ -49,6 +49,6 @@ launchpadlib>=1.10.16
49
49
 
50
50
  [types]
51
51
  mypy[reports]==1.9.0
52
- pyright==1.1.353
52
+ pyright==1.1.356
53
53
  types-requests
54
54
  types-urllib3
@@ -45,13 +45,13 @@ remote = [
45
45
  "launchpadlib>=1.10.16",
46
46
  ]
47
47
  dev = [
48
- "coverage[toml]==7.4.3",
48
+ "coverage[toml]==7.4.4",
49
49
  "hypothesis>=6.0",
50
50
  "pyfakefs~=5.3",
51
51
  "pytest==8.1.1",
52
52
  "pytest-check==2.3.1",
53
53
  "pytest-cov==5.0.0",
54
- "pytest-mock==3.12.0",
54
+ "pytest-mock==3.14.0",
55
55
  "pytest-rerunfailures==13.0",
56
56
  "pytest-time>=0.3.1",
57
57
  "responses~=0.25.0",
@@ -64,7 +64,7 @@ lint = [
64
64
  ]
65
65
  types = [
66
66
  "mypy[reports]==1.9.0",
67
- "pyright==1.1.353",
67
+ "pyright==1.1.356",
68
68
  "types-requests",
69
69
  "types-urllib3",
70
70
  ]
@@ -72,6 +72,18 @@ def features(request) -> dict[str, bool]:
72
72
  return features
73
73
 
74
74
 
75
+ @pytest.fixture(scope="session")
76
+ def default_app_metadata() -> craft_application.AppMetadata:
77
+ with pytest.MonkeyPatch.context() as m:
78
+ m.setattr(metadata, "version", lambda _: "3.14159")
79
+ return craft_application.AppMetadata(
80
+ "testcraft",
81
+ "A fake app for testing craft-application",
82
+ BuildPlannerClass=MyBuildPlanner,
83
+ source_ignore_patterns=["*.snap", "*.charm", "*.starcraft"],
84
+ )
85
+
86
+
75
87
  @pytest.fixture()
76
88
  def app_metadata(features) -> craft_application.AppMetadata:
77
89
  with pytest.MonkeyPatch.context() as m:
@@ -132,13 +144,25 @@ def full_build_plan(mocker) -> list[models.BuildInfo]:
132
144
  return build_plan
133
145
 
134
146
 
147
+ @pytest.fixture()
148
+ def enable_partitions() -> Iterator[craft_parts.Features]:
149
+ """Enable the partitions feature in craft_parts for the relevant test."""
150
+ enable_overlay = craft_parts.Features().enable_overlay
151
+
152
+ craft_parts.Features.reset()
153
+ yield craft_parts.Features(enable_overlay=enable_overlay, enable_partitions=True)
154
+ craft_parts.Features.reset()
155
+
156
+
135
157
  @pytest.fixture()
136
158
  def enable_overlay() -> Iterator[craft_parts.Features]:
137
159
  """Enable the overlay feature in craft_parts for the relevant test."""
138
160
  if not os.getenv("CI") and not shutil.which("fuse-overlayfs"):
139
161
  pytest.skip("fuse-overlayfs not installed, skipping overlay tests.")
162
+
163
+ enable_partitions = craft_parts.Features().enable_partitions
140
164
  craft_parts.Features.reset()
141
- yield craft_parts.Features(enable_overlay=True)
165
+ yield craft_parts.Features(enable_overlay=True, enable_partitions=enable_partitions)
142
166
  craft_parts.Features.reset()
143
167
 
144
168
 
@@ -18,9 +18,11 @@ import os
18
18
  import pathlib
19
19
  import sys
20
20
  import tempfile
21
+ from unittest import mock
21
22
 
22
23
  import pytest
23
- from craft_application.services import provider
24
+ from craft_application import launchpad
25
+ from craft_application.services import provider, remotebuild
24
26
  from craft_providers import lxd, multipass
25
27
 
26
28
 
@@ -53,6 +55,14 @@ def provider_service(app_metadata, fake_project, fake_build_plan, fake_services)
53
55
  )
54
56
 
55
57
 
58
+ @pytest.fixture(scope="session")
59
+ def anonymous_remote_build_service(default_app_metadata):
60
+ """Provider service with install snap disabled for integration tests"""
61
+ service = remotebuild.RemoteBuildService(default_app_metadata, services=mock.Mock())
62
+ service.lp = launchpad.Launchpad.anonymous("testcraft")
63
+ return service
64
+
65
+
56
66
  @pytest.fixture()
57
67
  def snap_safe_tmp_path():
58
68
  """A temporary path accessible to snap-confined craft providers.
@@ -0,0 +1,47 @@
1
+ # This file is part of craft-application.
2
+ #
3
+ # Copyright 2024 Canonical Ltd.
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify it
6
+ # under the terms of the GNU Lesser General Public License version 3, as
7
+ # published by the Free Software Foundation.
8
+ #
9
+ # This program is distributed in the hope that it will be useful, but WITHOUT
10
+ # ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11
+ # SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.
12
+ # See the GNU Lesser General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Lesser General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+ """Tests for the remote build service."""
17
+
18
+ import pytest
19
+ from craft_application import errors
20
+
21
+
22
+ @pytest.fixture(scope="module", params=["charmcraft", "coreutils", "dpkg", "sudo"])
23
+ def public_project_name(request):
24
+ return request.param
25
+
26
+
27
+ def test_use_public_project(anonymous_remote_build_service, public_project_name):
28
+ """Test that we can get a real (public) project using an anonymous client."""
29
+ anonymous_remote_build_service.set_project(public_project_name)
30
+
31
+ assert anonymous_remote_build_service._lp_project.name == public_project_name
32
+
33
+
34
+ def test_error_with_nonexistent_project(anonymous_remote_build_service):
35
+ """Test failing gracefully with a nonexistent project."""
36
+ name = "this launchpad project does not exist!"
37
+
38
+ with pytest.raises(
39
+ errors.CraftError, match=f"Could not find project on Launchpad: {name}"
40
+ ):
41
+ anonymous_remote_build_service.set_project(name)
42
+
43
+
44
+ def test_project_is_public(anonymous_remote_build_service, public_project_name):
45
+ """Test that the given project is public."""
46
+ anonymous_remote_build_service.set_project(public_project_name)
47
+ assert not anonymous_remote_build_service.is_project_private()
@@ -306,6 +306,26 @@ def test_init_with_feature_package_repositories(
306
306
  assert service._lcm._project_info.package_repositories == package_repositories
307
307
 
308
308
 
309
+ @pytest.mark.usefixtures("enable_partitions")
310
+ def test_init_with_partitions(
311
+ app_metadata, fake_project, fake_services, tmp_path, fake_build_plan
312
+ ):
313
+ service = lifecycle.LifecycleService(
314
+ app_metadata,
315
+ fake_services,
316
+ project=fake_project,
317
+ work_dir=tmp_path,
318
+ cache_dir=tmp_path,
319
+ platform=None,
320
+ build_plan=fake_build_plan,
321
+ partitions=["default", "mypartition"],
322
+ )
323
+ assert service._lcm is None
324
+ service.setup()
325
+ assert service._lcm is not None
326
+ assert service._lcm._project_info.partitions == ["default", "mypartition"]
327
+
328
+
309
329
  def test_prime_dir(lifecycle_service, tmp_path):
310
330
  prime_dir = lifecycle_service.prime_dir
311
331
 
@@ -290,7 +290,6 @@ def test_instance(
290
290
  provider_service,
291
291
  base_name,
292
292
  allow_unstable,
293
- mocker,
294
293
  ):
295
294
  mock_provider = mock.MagicMock(spec=craft_providers.Provider)
296
295
  monkeypatch.setattr(
@@ -298,7 +297,6 @@ def test_instance(
298
297
  "get_provider",
299
298
  lambda name: mock_provider, # noqa: ARG005 (unused argument)
300
299
  )
301
- spy_pause = mocker.spy(provider.emit, "pause")
302
300
  arch = util.get_host_architecture()
303
301
  build_info = models.BuildInfo("foo", arch, arch, base_name)
304
302
 
@@ -326,8 +324,6 @@ def test_instance(
326
324
  )
327
325
  with check:
328
326
  emitter.assert_progress("Launching managed .+ instance...", regex=True)
329
- with check:
330
- assert spy_pause.call_count == 1
331
327
 
332
328
 
333
329
  def test_load_bashrc(emitter):
@@ -21,7 +21,7 @@ from unittest import mock
21
21
  import lazr.restfulclient.errors
22
22
  import lazr.restfulclient.resource
23
23
  import pytest
24
- from craft_application import launchpad
24
+ from craft_application import errors, launchpad
25
25
  from craft_application.remote import git
26
26
 
27
27
  from tests.unit.services.conftest import (
@@ -41,6 +41,35 @@ def mock_lp_project(fake_launchpad, mock_project_entry):
41
41
  return launchpad.models.Project(fake_launchpad, mock_project_entry)
42
42
 
43
43
 
44
+ @pytest.mark.parametrize("name", ["some-project", "another-project"])
45
+ def test_set_project_success(remote_build_service, mock_project_entry, name):
46
+ mock_project_entry.name = name
47
+ remote_build_service.lp.lp.projects = {name: mock_project_entry}
48
+
49
+ remote_build_service.set_project(name)
50
+ project = remote_build_service._ensure_project()
51
+
52
+ assert project.name == name
53
+
54
+
55
+ @pytest.mark.parametrize("name", ["some-project", "another-project"])
56
+ def test_set_project_name_error(remote_build_service, mock_project_entry, name):
57
+ mock_project_entry.name = name
58
+ remote_build_service.lp.lp.projects.__getitem__.side_effect = (
59
+ lazr.restfulclient.errors.NotFound("yo", "dawg")
60
+ )
61
+
62
+ with pytest.raises(
63
+ errors.CraftError, match=f"Could not find project on Launchpad: {name}"
64
+ ) as exc_info:
65
+ remote_build_service.set_project(name)
66
+
67
+ assert (
68
+ exc_info.value.resolution
69
+ == "Ensure the project exists and that you have access to it."
70
+ )
71
+
72
+
44
73
  def test_ensure_project_existing(remote_build_service, mock_project_entry):
45
74
  remote_build_service.lp.lp.projects = {
46
75
  "craft_test_user-craft-remote-build": mock_project_entry
@@ -57,6 +86,35 @@ def test_ensure_project_new(remote_build_service):
57
86
  remote_build_service._ensure_project()
58
87
 
59
88
 
89
+ @pytest.mark.parametrize(
90
+ ("information_type", "expected"),
91
+ [
92
+ (launchpad.models.InformationType.PUBLIC, False),
93
+ (launchpad.models.InformationType.PUBLIC_SECURITY, False),
94
+ (launchpad.models.InformationType.PRIVATE, True),
95
+ (launchpad.models.InformationType.PRIVATE_SECURITY, True),
96
+ (launchpad.models.InformationType.PROPRIETARY, True),
97
+ (launchpad.models.InformationType.EMBARGOED, True),
98
+ ],
99
+ )
100
+ def test_is_project_private(
101
+ remote_build_service, mock_project_entry, information_type, expected
102
+ ):
103
+ mock_project_entry.information_type = str(information_type.value)
104
+ remote_build_service.lp.lp.projects = {"test-project": mock_project_entry}
105
+
106
+ remote_build_service.set_project("test-project")
107
+ assert remote_build_service.is_project_private() == expected
108
+
109
+
110
+ def test_is_project_private_error(remote_build_service):
111
+ with pytest.raises(
112
+ RuntimeError,
113
+ match="Cannot check if the project is private before setting its name.",
114
+ ):
115
+ remote_build_service.is_project_private()
116
+
117
+
60
118
  def test_new_repository(
61
119
  monkeypatch, tmp_path, remote_build_service, mock_lp_project, mock_push_url
62
120
  ):