craft-application 2.3.0__tar.gz → 2.5.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 (168) hide show
  1. {craft-application-2.3.0 → craft_application-2.5}/PKG-INFO +4 -4
  2. {craft-application-2.3.0 → craft_application-2.5}/craft_application/_version.py +2 -2
  3. {craft-application-2.3.0 → craft_application-2.5}/craft_application/grammar.py +46 -13
  4. {craft-application-2.3.0 → craft_application-2.5}/craft_application/launchpad/models/project.py +2 -1
  5. {craft-application-2.3.0 → craft_application-2.5}/craft_application/models/grammar.py +31 -4
  6. {craft-application-2.3.0 → craft_application-2.5}/craft_application/remote/git.py +4 -1
  7. {craft-application-2.3.0 → craft_application-2.5}/craft_application/services/remotebuild.py +83 -22
  8. {craft-application-2.3.0 → craft_application-2.5}/craft_application.egg-info/PKG-INFO +4 -4
  9. {craft-application-2.3.0 → craft_application-2.5}/craft_application.egg-info/SOURCES.txt +1 -0
  10. {craft-application-2.3.0 → craft_application-2.5}/craft_application.egg-info/requires.txt +3 -3
  11. {craft-application-2.3.0 → craft_application-2.5}/pyproject.toml +7 -4
  12. {craft-application-2.3.0 → craft_application-2.5}/tests/conftest.py +12 -0
  13. {craft-application-2.3.0 → craft_application-2.5}/tests/integration/conftest.py +11 -1
  14. craft_application-2.5/tests/integration/data/valid_projects/grammar/testcraft.yaml +21 -0
  15. {craft-application-2.3.0 → craft_application-2.5}/tests/integration/launchpad/test_anonymous_access.py +11 -10
  16. craft_application-2.5/tests/integration/services/test_remotebuild.py +47 -0
  17. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/services/conftest.py +10 -0
  18. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/services/test_remotebuild.py +127 -3
  19. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/test_application.py +443 -20
  20. craft-application-2.3.0/tests/integration/data/valid_projects/grammar/testcraft.yaml +0 -11
  21. {craft-application-2.3.0 → craft_application-2.5}/.editorconfig +0 -0
  22. {craft-application-2.3.0 → craft_application-2.5}/.github/.jira_sync_config.yaml +0 -0
  23. {craft-application-2.3.0 → craft_application-2.5}/.github/ISSUE_TEMPLATE/bug.yaml +0 -0
  24. {craft-application-2.3.0 → craft_application-2.5}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  25. {craft-application-2.3.0 → craft_application-2.5}/.github/ISSUE_TEMPLATE/task.yaml +0 -0
  26. {craft-application-2.3.0 → craft_application-2.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  27. {craft-application-2.3.0 → craft_application-2.5}/.github/release-drafter.yaml +0 -0
  28. {craft-application-2.3.0 → craft_application-2.5}/.github/release-drafter.yml +0 -0
  29. {craft-application-2.3.0 → craft_application-2.5}/.github/renovate.json5 +0 -0
  30. {craft-application-2.3.0 → craft_application-2.5}/.github/workflows/cla-check.yaml +0 -0
  31. {craft-application-2.3.0 → craft_application-2.5}/.github/workflows/docs.yaml +0 -0
  32. {craft-application-2.3.0 → craft_application-2.5}/.github/workflows/release-drafter.yaml +0 -0
  33. {craft-application-2.3.0 → craft_application-2.5}/.github/workflows/tests.yaml +0 -0
  34. {craft-application-2.3.0 → craft_application-2.5}/.gitignore +0 -0
  35. {craft-application-2.3.0 → craft_application-2.5}/.pre-commit-config.yaml +0 -0
  36. {craft-application-2.3.0 → craft_application-2.5}/.readthedocs.yaml +0 -0
  37. {craft-application-2.3.0 → craft_application-2.5}/.yamllint.yaml +0 -0
  38. {craft-application-2.3.0 → craft_application-2.5}/HACKING.rst +0 -0
  39. {craft-application-2.3.0 → craft_application-2.5}/LICENSE +0 -0
  40. {craft-application-2.3.0 → craft_application-2.5}/README.rst +0 -0
  41. {craft-application-2.3.0 → craft_application-2.5}/craft_application/__init__.py +0 -0
  42. {craft-application-2.3.0 → craft_application-2.5}/craft_application/application.py +0 -0
  43. {craft-application-2.3.0 → craft_application-2.5}/craft_application/commands/__init__.py +0 -0
  44. {craft-application-2.3.0 → craft_application-2.5}/craft_application/commands/base.py +0 -0
  45. {craft-application-2.3.0 → craft_application-2.5}/craft_application/commands/lifecycle.py +0 -0
  46. {craft-application-2.3.0 → craft_application-2.5}/craft_application/commands/other.py +0 -0
  47. {craft-application-2.3.0 → craft_application-2.5}/craft_application/errors.py +0 -0
  48. {craft-application-2.3.0 → craft_application-2.5}/craft_application/launchpad/__init__.py +0 -0
  49. {craft-application-2.3.0 → craft_application-2.5}/craft_application/launchpad/errors.py +0 -0
  50. {craft-application-2.3.0 → craft_application-2.5}/craft_application/launchpad/launchpad.py +0 -0
  51. {craft-application-2.3.0 → craft_application-2.5}/craft_application/launchpad/models/__init__.py +0 -0
  52. {craft-application-2.3.0 → craft_application-2.5}/craft_application/launchpad/models/base.py +0 -0
  53. {craft-application-2.3.0 → craft_application-2.5}/craft_application/launchpad/models/build.py +0 -0
  54. {craft-application-2.3.0 → craft_application-2.5}/craft_application/launchpad/models/code.py +0 -0
  55. {craft-application-2.3.0 → craft_application-2.5}/craft_application/launchpad/models/distro.py +0 -0
  56. {craft-application-2.3.0 → craft_application-2.5}/craft_application/launchpad/models/recipe.py +0 -0
  57. {craft-application-2.3.0 → craft_application-2.5}/craft_application/launchpad/util.py +0 -0
  58. {craft-application-2.3.0 → craft_application-2.5}/craft_application/misc/instance_bashrc +0 -0
  59. {craft-application-2.3.0 → craft_application-2.5}/craft_application/models/__init__.py +0 -0
  60. {craft-application-2.3.0 → craft_application-2.5}/craft_application/models/base.py +0 -0
  61. {craft-application-2.3.0 → craft_application-2.5}/craft_application/models/constraints.py +0 -0
  62. {craft-application-2.3.0 → craft_application-2.5}/craft_application/models/metadata.py +0 -0
  63. {craft-application-2.3.0 → craft_application-2.5}/craft_application/models/project.py +0 -0
  64. {craft-application-2.3.0 → craft_application-2.5}/craft_application/py.typed +0 -0
  65. {craft-application-2.3.0 → craft_application-2.5}/craft_application/remote/__init__.py +0 -0
  66. {craft-application-2.3.0 → craft_application-2.5}/craft_application/remote/errors.py +0 -0
  67. {craft-application-2.3.0 → craft_application-2.5}/craft_application/remote/utils.py +0 -0
  68. {craft-application-2.3.0 → craft_application-2.5}/craft_application/remote/worktree.py +0 -0
  69. {craft-application-2.3.0 → craft_application-2.5}/craft_application/secrets.py +0 -0
  70. {craft-application-2.3.0 → craft_application-2.5}/craft_application/services/__init__.py +0 -0
  71. {craft-application-2.3.0 → craft_application-2.5}/craft_application/services/base.py +0 -0
  72. {craft-application-2.3.0 → craft_application-2.5}/craft_application/services/lifecycle.py +0 -0
  73. {craft-application-2.3.0 → craft_application-2.5}/craft_application/services/package.py +0 -0
  74. {craft-application-2.3.0 → craft_application-2.5}/craft_application/services/provider.py +0 -0
  75. {craft-application-2.3.0 → craft_application-2.5}/craft_application/services/request.py +0 -0
  76. {craft-application-2.3.0 → craft_application-2.5}/craft_application/services/service_factory.py +0 -0
  77. {craft-application-2.3.0 → craft_application-2.5}/craft_application/util/__init__.py +0 -0
  78. {craft-application-2.3.0 → craft_application-2.5}/craft_application/util/callbacks.py +0 -0
  79. {craft-application-2.3.0 → craft_application-2.5}/craft_application/util/error_formatting.py +0 -0
  80. {craft-application-2.3.0 → craft_application-2.5}/craft_application/util/logging.py +0 -0
  81. {craft-application-2.3.0 → craft_application-2.5}/craft_application/util/paths.py +0 -0
  82. {craft-application-2.3.0 → craft_application-2.5}/craft_application/util/platforms.py +0 -0
  83. {craft-application-2.3.0 → craft_application-2.5}/craft_application/util/repositories.py +0 -0
  84. {craft-application-2.3.0 → craft_application-2.5}/craft_application/util/snap_config.py +0 -0
  85. {craft-application-2.3.0 → craft_application-2.5}/craft_application/util/string.py +0 -0
  86. {craft-application-2.3.0 → craft_application-2.5}/craft_application/util/yaml.py +0 -0
  87. {craft-application-2.3.0 → craft_application-2.5}/craft_application.egg-info/dependency_links.txt +0 -0
  88. {craft-application-2.3.0 → craft_application-2.5}/craft_application.egg-info/top_level.txt +0 -0
  89. {craft-application-2.3.0 → craft_application-2.5}/docs/_static/css/custom.css +0 -0
  90. {craft-application-2.3.0 → craft_application-2.5}/docs/conf.py +0 -0
  91. {craft-application-2.3.0 → craft_application-2.5}/docs/explanation/index.rst +0 -0
  92. {craft-application-2.3.0 → craft_application-2.5}/docs/howto/index.rst +0 -0
  93. {craft-application-2.3.0 → craft_application-2.5}/docs/index.rst +0 -0
  94. {craft-application-2.3.0 → craft_application-2.5}/docs/reference/index.rst +0 -0
  95. {craft-application-2.3.0 → craft_application-2.5}/docs/tutorials/index.rst +0 -0
  96. {craft-application-2.3.0 → craft_application-2.5}/setup.cfg +0 -0
  97. {craft-application-2.3.0 → craft_application-2.5}/tests/__init__.py +0 -0
  98. {craft-application-2.3.0 → craft_application-2.5}/tests/integration/__init__.py +0 -0
  99. {craft-application-2.3.0 → craft_application-2.5}/tests/integration/data/build-secrets/secret-source-folder/source-file.txt +0 -0
  100. {craft-application-2.3.0 → craft_application-2.5}/tests/integration/data/build-secrets/testcraft.yaml +0 -0
  101. {craft-application-2.3.0 → craft_application-2.5}/tests/integration/data/invalid_projects/build-error/testcraft.yaml +0 -0
  102. {craft-application-2.3.0 → craft_application-2.5}/tests/integration/data/valid_projects/adoption/stderr +0 -0
  103. {craft-application-2.3.0 → craft_application-2.5}/tests/integration/data/valid_projects/adoption/testcraft.yaml +0 -0
  104. {craft-application-2.3.0 → craft_application-2.5}/tests/integration/data/valid_projects/basic/stderr +0 -0
  105. {craft-application-2.3.0 → craft_application-2.5}/tests/integration/data/valid_projects/basic/testcraft.yaml +0 -0
  106. {craft-application-2.3.0 → craft_application-2.5}/tests/integration/data/valid_projects/environment/stderr +0 -0
  107. {craft-application-2.3.0 → craft_application-2.5}/tests/integration/data/valid_projects/environment/testcraft.yaml +0 -0
  108. {craft-application-2.3.0 → craft_application-2.5}/tests/integration/data/valid_projects/grammar/src/on-amd64-to-amd64/hello.txt +0 -0
  109. {craft-application-2.3.0 → craft_application-2.5}/tests/integration/data/valid_projects/grammar/src/on-amd64-to-arm64/hello.txt +0 -0
  110. {craft-application-2.3.0 → craft_application-2.5}/tests/integration/data/valid_projects/grammar/stderr +0 -0
  111. {craft-application-2.3.0 → craft_application-2.5}/tests/integration/launchpad/__init__.py +0 -0
  112. {craft-application-2.3.0 → craft_application-2.5}/tests/integration/launchpad/conftest.py +0 -0
  113. {craft-application-2.3.0 → craft_application-2.5}/tests/integration/services/__init__.py +0 -0
  114. {craft-application-2.3.0 → craft_application-2.5}/tests/integration/services/test_lifecycle.py +0 -0
  115. {craft-application-2.3.0 → craft_application-2.5}/tests/integration/services/test_provider.py +0 -0
  116. {craft-application-2.3.0 → craft_application-2.5}/tests/integration/services/test_request.py +0 -0
  117. {craft-application-2.3.0 → craft_application-2.5}/tests/integration/services/test_service_factory.py +0 -0
  118. {craft-application-2.3.0 → craft_application-2.5}/tests/integration/test_application.py +0 -0
  119. {craft-application-2.3.0 → craft_application-2.5}/tests/integration/test_version.py +0 -0
  120. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/__init__.py +0 -0
  121. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/commands/__init__.py +0 -0
  122. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/commands/test_base.py +0 -0
  123. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/commands/test_lifecycle.py +0 -0
  124. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/commands/test_other.py +0 -0
  125. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/conftest.py +0 -0
  126. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/launchpad/__init__.py +0 -0
  127. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/launchpad/conftest.py +0 -0
  128. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/launchpad/models/__init__.py +0 -0
  129. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/launchpad/models/test_base.py +0 -0
  130. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/launchpad/models/test_code.py +0 -0
  131. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/launchpad/test_launchpad.py +0 -0
  132. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/launchpad/test_util.py +0 -0
  133. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/models/__init__.py +0 -0
  134. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/models/project_models/basic_project.yaml +0 -0
  135. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/models/project_models/full_project.yaml +0 -0
  136. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/models/project_models/invalid_project.yaml +0 -0
  137. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/models/test_constraints.py +0 -0
  138. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/models/test_project.py +0 -0
  139. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/remote/__init__.py +0 -0
  140. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/remote/conftest.py +0 -0
  141. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/remote/test_errors.py +0 -0
  142. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/remote/test_git.py +0 -0
  143. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/remote/test_utils.py +0 -0
  144. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/remote/test_worktree.py +0 -0
  145. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/services/__init__.py +0 -0
  146. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/services/test_lifecycle.py +0 -0
  147. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/services/test_package.py +0 -0
  148. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/services/test_provider.py +0 -0
  149. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/services/test_repositories.py +0 -0
  150. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/services/test_request.py +0 -0
  151. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/services/test_service_factory.py +0 -0
  152. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/test_errors.py +0 -0
  153. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/test_grammar.py +0 -0
  154. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/test_nothing.py +0 -0
  155. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/test_secrets.py +0 -0
  156. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/util/__init__.py +0 -0
  157. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/util/invalid_yaml/_README +0 -0
  158. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/util/invalid_yaml/duplicate_second_level.yaml-invalid +0 -0
  159. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/util/invalid_yaml/duplicate_top_level.yaml-invalid +0 -0
  160. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/util/invalid_yaml/unhashable.yaml-invalid +0 -0
  161. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/util/test_error_formatting.py +0 -0
  162. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/util/test_logging.py +0 -0
  163. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/util/test_paths.py +0 -0
  164. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/util/test_snap_config.py +0 -0
  165. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/util/test_string.py +0 -0
  166. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/util/test_yaml.py +0 -0
  167. {craft-application-2.3.0 → craft_application-2.5}/tests/unit/util/valid_yaml/empty.yaml +0 -0
  168. {craft-application-2.3.0 → craft_application-2.5}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: craft-application
3
- Version: 2.3.0
3
+ Version: 2.5.0
4
4
  Summary: A framework for *craft applications.
5
5
  Classifier: Development Status :: 5 - Production/Stable
6
6
  Classifier: Environment :: Console
@@ -17,7 +17,7 @@ Description-Content-Type: text/x-rst
17
17
  License-File: LICENSE
18
18
  Requires-Dist: craft-archives>=1.1.3
19
19
  Requires-Dist: craft-cli>=2.4.0
20
- Requires-Dist: craft-grammar>=1.1.1
20
+ Requires-Dist: craft-grammar>=1.2.0
21
21
  Requires-Dist: craft-parts>=1.21.1
22
22
  Requires-Dist: craft-providers<2.0,>=1.20.0
23
23
  Requires-Dist: snap-helpers>=0.4.2
@@ -37,7 +37,7 @@ 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
39
  Requires-Dist: pytest-mock==3.14.0; extra == "dev"
40
- Requires-Dist: pytest-rerunfailures==13.0; extra == "dev"
40
+ Requires-Dist: pytest-rerunfailures==14.0; extra == "dev"
41
41
  Requires-Dist: pytest-time>=0.3.1; extra == "dev"
42
42
  Requires-Dist: responses~=0.25.0; extra == "dev"
43
43
  Requires-Dist: craft-application[remote]; 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.356; extra == "types"
50
+ Requires-Dist: pyright==1.1.358; 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.3.0'
16
- __version_tuple__ = version_tuple = (2, 3, 0)
15
+ __version__ = version = '2.5.0'
16
+ __version_tuple__ = version_tuple = (2, 5, 0)
@@ -15,38 +15,71 @@
15
15
  # with this program. If not, see <http://www.gnu.org/licenses/>.
16
16
  """Grammar processor."""
17
17
 
18
- from typing import Any
18
+ from typing import Any, cast
19
19
 
20
+ import craft_cli
20
21
  from craft_grammar import GrammarProcessor # type: ignore[import-untyped]
22
+ from craft_grammar.errors import GrammarSyntaxError # type: ignore[import-untyped]
21
23
 
22
- _KEYS = [
23
- "source",
24
+ from craft_application.errors import CraftValidationError
25
+
26
+ # Values that should return as a single object / list / dict.
27
+ _NON_SCALAR_VALUES = [
28
+ "source-submodules",
29
+ "after",
30
+ "overlay-packages",
31
+ "build-attributes",
32
+ "permissions",
24
33
  "build-environment",
25
34
  "build-packages",
26
35
  "stage-packages",
27
36
  "build-snaps",
28
37
  "stage-snaps",
38
+ "overlay",
39
+ "organize",
40
+ "stage",
41
+ "prime",
29
42
  ]
30
43
 
31
- _SCALAR_VALUES = ["source"]
44
+ # Values that should return a dict, not in a list.
45
+ _DICT_ONLY_VALUES = [
46
+ "organize",
47
+ ]
32
48
 
33
49
 
34
50
  def process_part(
35
51
  *, part_yaml_data: dict[str, Any], processor: GrammarProcessor
36
52
  ) -> dict[str, Any]:
37
53
  """Process grammar for a given part."""
38
- existing_keys = (key for key in _KEYS if key in part_yaml_data)
39
-
40
- for key in existing_keys:
54
+ for key in part_yaml_data:
41
55
  unprocessed_grammar = part_yaml_data[key]
42
-
43
- if key in _SCALAR_VALUES and isinstance(unprocessed_grammar, str):
56
+ craft_cli.emit.debug(f"Processing grammar for {key}: {unprocessed_grammar}")
57
+
58
+ # grammar aware models can only be a string or list of dict, skip any other type.
59
+ if isinstance(unprocessed_grammar, list):
60
+ if any(not isinstance(d, dict) for d in unprocessed_grammar): # type: ignore[reportUnknownVariableType]
61
+ continue
62
+ if any(not isinstance(k, str) for d in unprocessed_grammar for k in d): # type: ignore[reportUnknownVariableType]
63
+ continue
64
+ unprocessed_grammar = cast(list[dict[str, Any]], unprocessed_grammar)
65
+ elif isinstance(unprocessed_grammar, str):
44
66
  unprocessed_grammar = [unprocessed_grammar]
45
-
46
- processed_grammar = processor.process(grammar=unprocessed_grammar)
47
-
48
- if key in _SCALAR_VALUES:
67
+ else:
68
+ continue
69
+
70
+ try:
71
+ processed_grammar = processor.process(grammar=unprocessed_grammar)
72
+ except GrammarSyntaxError as e:
73
+ raise CraftValidationError(
74
+ f"Invalid grammar syntax while processing '{key}' in '{part_yaml_data}': {e}"
75
+ ) from e
76
+
77
+ # special cases
78
+ # scalar values should return as a single object, not in a list.
79
+ # dict values should return as a dict, not in a list.
80
+ if key not in _NON_SCALAR_VALUES or key in _DICT_ONLY_VALUES:
49
81
  processed_grammar = processed_grammar[0] if processed_grammar else None
82
+
50
83
  part_yaml_data[key] = processed_grammar
51
84
 
52
85
  return part_yaml_data
@@ -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]
@@ -19,6 +19,10 @@ from typing import Any
19
19
 
20
20
  import pydantic
21
21
  from craft_grammar.models import ( # type: ignore[import-untyped]
22
+ GrammarBool,
23
+ GrammarDict,
24
+ GrammarDictList,
25
+ GrammarInt,
22
26
  GrammarSingleEntryDictList,
23
27
  GrammarStr,
24
28
  GrammarStrList,
@@ -38,13 +42,36 @@ class _GrammarAwareModel(pydantic.BaseModel):
38
42
 
39
43
 
40
44
  class _GrammarAwarePart(_GrammarAwareModel):
45
+ plugin: GrammarStr | None
41
46
  source: GrammarStr | None
42
- build_environment: GrammarSingleEntryDictList | None
43
- build_packages: GrammarStrList | None
47
+ source_checksum: GrammarStr | None
48
+ source_branch: GrammarStr | None
49
+ source_commit: GrammarStr | None
50
+ source_depth: GrammarInt | None
51
+ source_subdir: GrammarStr | None
52
+ source_submodules: GrammarStrList | None
53
+ source_tag: GrammarStr | None
54
+ source_type: GrammarStr | None
55
+ disable_parallel: GrammarBool | None
56
+ after: GrammarStrList | None
57
+ overlay_packages: GrammarStrList | None
58
+ stage_snaps: GrammarStrList | None
44
59
  stage_packages: GrammarStrList | None
45
60
  build_snaps: GrammarStrList | None
46
- stage_snaps: GrammarStrList | None
47
- parse_info: list[str] | None
61
+ build_packages: GrammarStrList | None
62
+ build_environment: GrammarSingleEntryDictList | None
63
+ build_attributes: GrammarStrList | None
64
+ organize_files: GrammarDict | None
65
+ overlay_files: GrammarStrList | None
66
+ stage_files: GrammarStrList | None
67
+ prime_files: GrammarStrList | None
68
+ override_pull: GrammarStr | None
69
+ overlay_script: GrammarStr | None
70
+ override_build: GrammarStr | None
71
+ override_stage: GrammarStr | None
72
+ override_prime: GrammarStr | None
73
+ permissions: GrammarDictList | None
74
+ parse_info: GrammarStrList | None
48
75
 
49
76
 
50
77
  class GrammarAwareProject(_GrammarAwareModel):
@@ -253,7 +253,10 @@ class GitRepo:
253
253
  # temporarily call git directly due to libgit2 bug that unable to push
254
254
  # large repos using https. See https://github.com/libgit2/libgit2/issues/6385
255
255
  # and https://github.com/snapcore/snapcraft/issues/4478
256
- cmd: list[str] = ["git", "push", remote_url, refspec, "--progress"]
256
+ # Force push in case this repository already exists. The repository is always
257
+ # going to exist solely for remote builds, so the only potential issue here is a
258
+ # race condition with multiple remote builds on the same machine.
259
+ cmd: list[str] = ["git", "push", "--force", remote_url, refspec, "--progress"]
257
260
  if push_tags:
258
261
  cmd.append("--tags")
259
262
 
@@ -16,6 +16,7 @@
16
16
  """Service class for remote build commands."""
17
17
  from __future__ import annotations
18
18
 
19
+ import contextlib
19
20
  import datetime
20
21
  import itertools
21
22
  import os
@@ -27,10 +28,16 @@ from typing import TYPE_CHECKING, Any, cast
27
28
  from urllib import parse
28
29
 
29
30
  import craft_cli
31
+ import launchpadlib.errors # type: ignore[import-untyped]
30
32
  import platformdirs
31
33
 
32
34
  from craft_application import errors, launchpad, models
33
- from craft_application.remote import GitRepo, WorkTree, utils
35
+ from craft_application.remote import (
36
+ GitRepo,
37
+ WorkTree,
38
+ check_git_repo_for_remote_build,
39
+ utils,
40
+ )
34
41
  from craft_application.services import base
35
42
 
36
43
  if TYPE_CHECKING: # pragma: no cover
@@ -62,6 +69,7 @@ class RemoteBuildService(base.AppService):
62
69
  self._repository: launchpad.models.GitRepository = None # type: ignore[assignment]
63
70
  self._recipe: launchpad.models.recipe.BaseRecipe = None # type: ignore[assignment]
64
71
  self._builds: Collection[launchpad.models.Build] = []
72
+ self._project_name: str | None = None
65
73
 
66
74
  def setup(self) -> None:
67
75
  """Set up the remote builder."""
@@ -69,6 +77,35 @@ class RemoteBuildService(base.AppService):
69
77
 
70
78
  # region Public API
71
79
  # Commands will call a subset of these methods, generally in order.
80
+ def set_project(self, name: str) -> None:
81
+ """Set the name of the project to use."""
82
+ if self._is_setup:
83
+ raise RuntimeError(
84
+ "Project name may only be set before starting or resuming builds."
85
+ )
86
+ try:
87
+ self._lp_project = self.lp.get_project(name)
88
+ except launchpad.errors.NotFoundError:
89
+ raise errors.CraftError(
90
+ message=f"Could not find project on Launchpad: {name}",
91
+ resolution="Ensure the project exists and that you have access to it.",
92
+ retcode=77, # EX_NOPERM from sysexits.h
93
+ reportable=False,
94
+ logpath_report=False,
95
+ )
96
+ self._project_name = name
97
+
98
+ def is_project_private(self) -> bool:
99
+ """Check whether the named project is private."""
100
+ if not self._lp_project:
101
+ raise RuntimeError(
102
+ "Cannot check if the project is private before setting its name."
103
+ )
104
+ return self._lp_project.information_type not in (
105
+ launchpad.models.InformationType.PUBLIC,
106
+ launchpad.models.InformationType.PUBLIC_SECURITY,
107
+ )
108
+
72
109
  def set_timeout(self, seconds_in_future: int) -> None:
73
110
  """Set the deadline to a certain number of seconds in the future."""
74
111
  self._deadline = time.monotonic_ns() + (seconds_in_future * 10**9)
@@ -85,10 +122,12 @@ class RemoteBuildService(base.AppService):
85
122
 
86
123
  project = cast(models.Project, self._services.project)
87
124
 
125
+ check_git_repo_for_remote_build(project_dir)
126
+
88
127
  self._name = utils.get_build_id(self._app.name, project.name, project_dir)
89
128
  self._lp_project = self._ensure_project()
90
- _, self._repository = self._new_repository(project_dir)
91
- self._recipe = self._new_recipe(
129
+ _, self._repository = self._ensure_repository(project_dir)
130
+ self._recipe = self._ensure_recipe(
92
131
  self._name, self._repository, architectures=architectures
93
132
  )
94
133
  self._check_timeout()
@@ -207,28 +246,32 @@ class RemoteBuildService(base.AppService):
207
246
 
208
247
  def _ensure_project(self) -> launchpad.models.Project:
209
248
  """Ensure that the user's remote build project exists."""
249
+ if self._lp_project:
250
+ return self._lp_project
210
251
  try:
211
- return launchpad.models.Project.get(
212
- self.lp, f"{self.lp.username}-craft-remote-build"
213
- )
252
+ return self.lp.get_project(f"{self.lp.username}-craft-remote-build")
214
253
  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.",
254
+ return self.lp.new_project(
255
+ name=f"{self.lp.username}-craft-remote-build",
256
+ title=f"*craft remote builds for {self.lp.username}",
257
+ display_name=f"{self.lp.username} remote builds",
258
+ summary=f"Automatically-generated project for housing {self.lp.username}'s remote builds in snapcraft, charmcraft, etc.",
221
259
  )
222
260
 
223
- def _new_repository(
261
+ def _ensure_repository(
224
262
  self, project_dir: pathlib.Path
225
263
  ) -> tuple[WorkTree, launchpad.models.GitRepository]:
226
- """Create a repository on the local machine and on Launchpad."""
264
+ """Create a repository on the local machine and ensure it's on Launchpad."""
227
265
  work_tree = WorkTree(self._app.name, self._name, project_dir)
228
266
  work_tree.init_repo()
229
- lp_repository = launchpad.models.GitRepository.new(
230
- self.lp, self._name, target=self._lp_project.name
231
- )
267
+ try:
268
+ lp_repository = self.lp.new_repository(
269
+ self._name, project=self._lp_project.name
270
+ )
271
+ except launchpadlib.errors.HTTPError:
272
+ lp_repository = self.lp.get_repository(
273
+ name=self._name, project=self._lp_project.name
274
+ )
232
275
 
233
276
  token = lp_repository.get_access_token(
234
277
  f"{self._app.name} {self._app.version} remote build",
@@ -247,9 +290,20 @@ class RemoteBuildService(base.AppService):
247
290
 
248
291
  def _get_repository(self) -> launchpad.models.GitRepository:
249
292
  """Get an existing repository on Launchpad."""
250
- return launchpad.models.GitRepository.get(
251
- self.lp, name=self._name, owner=self.lp.username
252
- )
293
+ return self.lp.get_repository(name=self._name, owner=self.lp.username)
294
+
295
+ def _ensure_recipe(
296
+ self,
297
+ name: str,
298
+ repository: launchpad.models.GitRepository,
299
+ **kwargs: Any, # noqa: ANN401
300
+ ) -> launchpad.models.Recipe:
301
+ """Get a recipe or create it if it doesn't exist."""
302
+ with contextlib.suppress(ValueError): # Recipe doesn't exist
303
+ recipe = self._get_recipe()
304
+ recipe.delete()
305
+
306
+ return self._new_recipe(name, repository, **kwargs)
253
307
 
254
308
  def _new_recipe(
255
309
  self,
@@ -257,9 +311,16 @@ class RemoteBuildService(base.AppService):
257
311
  repository: launchpad.models.GitRepository,
258
312
  **kwargs: Any, # noqa: ANN401
259
313
  ) -> launchpad.models.Recipe:
260
- """Create a new recipe."""
314
+ """Create a new recipe for the given repository."""
315
+ repository.lp_refresh() # Prevents a race condition on new repositories.
316
+ git_ref = parse.urlparse(str(repository.git_https_url)).path + "/+ref/main"
261
317
  return self.RecipeClass.new(
262
- self.lp, name, self.lp.username, git_ref=repository.git_https_url, **kwargs
318
+ self.lp,
319
+ name,
320
+ self.lp.username,
321
+ git_ref=git_ref,
322
+ project=self._lp_project.name,
323
+ **kwargs,
263
324
  )
264
325
 
265
326
  def _get_recipe(self) -> launchpad.models.Recipe:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: craft-application
3
- Version: 2.3.0
3
+ Version: 2.5.0
4
4
  Summary: A framework for *craft applications.
5
5
  Classifier: Development Status :: 5 - Production/Stable
6
6
  Classifier: Environment :: Console
@@ -17,7 +17,7 @@ Description-Content-Type: text/x-rst
17
17
  License-File: LICENSE
18
18
  Requires-Dist: craft-archives>=1.1.3
19
19
  Requires-Dist: craft-cli>=2.4.0
20
- Requires-Dist: craft-grammar>=1.1.1
20
+ Requires-Dist: craft-grammar>=1.2.0
21
21
  Requires-Dist: craft-parts>=1.21.1
22
22
  Requires-Dist: craft-providers<2.0,>=1.20.0
23
23
  Requires-Dist: snap-helpers>=0.4.2
@@ -37,7 +37,7 @@ 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
39
  Requires-Dist: pytest-mock==3.14.0; extra == "dev"
40
- Requires-Dist: pytest-rerunfailures==13.0; extra == "dev"
40
+ Requires-Dist: pytest-rerunfailures==14.0; extra == "dev"
41
41
  Requires-Dist: pytest-time>=0.3.1; extra == "dev"
42
42
  Requires-Dist: responses~=0.25.0; extra == "dev"
43
43
  Requires-Dist: craft-application[remote]; 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.356; extra == "types"
50
+ Requires-Dist: pyright==1.1.358; 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
@@ -1,6 +1,6 @@
1
1
  craft-archives>=1.1.3
2
2
  craft-cli>=2.4.0
3
- craft-grammar>=1.1.1
3
+ craft-grammar>=1.2.0
4
4
  craft-parts>=1.21.1
5
5
  craft-providers<2.0,>=1.20.0
6
6
  snap-helpers>=0.4.2
@@ -24,7 +24,7 @@ pytest==8.1.1
24
24
  pytest-check==2.3.1
25
25
  pytest-cov==5.0.0
26
26
  pytest-mock==3.14.0
27
- pytest-rerunfailures==13.0
27
+ pytest-rerunfailures==14.0
28
28
  pytest-time>=0.3.1
29
29
  responses~=0.25.0
30
30
  craft-application[remote]
@@ -49,6 +49,6 @@ launchpadlib>=1.10.16
49
49
 
50
50
  [types]
51
51
  mypy[reports]==1.9.0
52
- pyright==1.1.356
52
+ pyright==1.1.358
53
53
  types-requests
54
54
  types-urllib3
@@ -5,7 +5,7 @@ dynamic = ["version", "readme"]
5
5
  dependencies = [
6
6
  "craft-archives>=1.1.3",
7
7
  "craft-cli>=2.4.0",
8
- "craft-grammar>=1.1.1",
8
+ "craft-grammar>=1.2.0",
9
9
  "craft-parts>=1.21.1",
10
10
  "craft-providers>=1.20.0,<2.0",
11
11
  "snap-helpers>=0.4.2",
@@ -52,7 +52,7 @@ dev = [
52
52
  "pytest-check==2.3.1",
53
53
  "pytest-cov==5.0.0",
54
54
  "pytest-mock==3.14.0",
55
- "pytest-rerunfailures==13.0",
55
+ "pytest-rerunfailures==14.0",
56
56
  "pytest-time>=0.3.1",
57
57
  "responses~=0.25.0",
58
58
  "craft-application[remote]"
@@ -64,7 +64,7 @@ lint = [
64
64
  ]
65
65
  types = [
66
66
  "mypy[reports]==1.9.0",
67
- "pyright==1.1.356",
67
+ "pyright==1.1.358",
68
68
  "types-requests",
69
69
  "types-urllib3",
70
70
  ]
@@ -84,7 +84,7 @@ apt = [
84
84
 
85
85
  [build-system]
86
86
  requires = [
87
- "setuptools==69.1.1",
87
+ "setuptools==69.4.0",
88
88
  "setuptools_scm[toml]>=7.1"
89
89
  ]
90
90
  build-backend = "setuptools.build_meta"
@@ -286,6 +286,9 @@ lint.ignore = [
286
286
  ]
287
287
  # isort leaves init files alone by default, this makes ruff ignore them too.
288
288
  "__init__.py" = ["I001"]
289
+ # TODO: Revert this
290
+ # https://github.com/canonical/craft-application/issues/306
291
+ "tests/integration/launchpad/test_anonymous_access.py" = ["ERA001"]
289
292
 
290
293
  [tool.ruff.lint.pep8-naming]
291
294
  classmethod-decorators = [
@@ -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:
@@ -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,21 @@
1
+ name: empty
2
+ title: A most basic project
3
+ version: 1.0
4
+ base: ["ubuntu", "22.04"]
5
+
6
+ parts:
7
+ hello-world:
8
+ plugin: dump
9
+ source:
10
+ - on amd64 to amd64: src/on-amd64-to-amd64
11
+ - on amd64 to arm64: src/on-amd64-to-arm64
12
+ organize:
13
+ - on amd64 to amd64:
14
+ hello.txt: hello-amd64.txt
15
+ - on amd64 to arm64:
16
+ hello.txt: hello-arm64.txt
17
+ build-environment:
18
+ - on amd64 to amd64:
19
+ - MY_VAR: amd64
20
+ - on amd64 to arm64:
21
+ - MY_VAR: arm64
@@ -2,7 +2,6 @@
2
2
 
3
3
  import pytest
4
4
  from craft_application import launchpad
5
- from craft_application.launchpad import models
6
5
 
7
6
 
8
7
  def test_anonymous_login(tmp_path):
@@ -16,15 +15,17 @@ def test_anonymous_login(tmp_path):
16
15
  assert cache_dir.is_dir()
17
16
 
18
17
 
19
- def test_get_basic_items(anonymous_lp):
20
- snapstore_server = anonymous_lp.get_project("snapstore-server")
21
- assert snapstore_server.name == "snapstore-server"
22
- assert snapstore_server.title == "Snap Store Server"
23
-
24
- snap_recipes = list(models.SnapRecipe.find(anonymous_lp, owner="lengau"))
25
- assert len(snap_recipes) > 0
26
- for recipe in snap_recipes:
27
- assert recipe.owner_name == "lengau"
18
+ # TODO: Re-enable this
19
+ # https://github.com/canonical/craft-application/issues/306
20
+ # def test_get_basic_items(anonymous_lp):
21
+ # snapstore_server = anonymous_lp.get_project("snapstore-server")
22
+ # assert snapstore_server.name == "snapstore-server"
23
+ # assert snapstore_server.title == "Snap Store Server"
24
+ #
25
+ # snap_recipes = list(models.SnapRecipe.find(anonymous_lp, owner="lengau"))
26
+ # assert len(snap_recipes) > 0
27
+ # for recipe in snap_recipes:
28
+ # assert recipe.owner_name == "lengau"
28
29
 
29
30
 
30
31
  @pytest.mark.parametrize(
@@ -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()