Merge ~jugmac00/lpci:add-pre-post-run-hooks into lpci:main

Proposed by Jürgen Gmach
Status: Merged
Merged at revision: 185c673ec63960aa05e9b0b7e8e6f44660c6f799
Proposed branch: ~jugmac00/lpci:add-pre-post-run-hooks
Merge into: lpci:main
Diff against target: 374 lines (+174/-82)
6 files modified
NEWS.rst (+4/-1)
docs/configuration.rst (+8/-0)
lpcraft/commands/run.py (+144/-81)
lpcraft/config.py (+2/-0)
lpcraft/plugin/hookspecs.py (+12/-0)
lpcraft/tests/test_config.py (+4/-0)
Reviewer Review Type Date Requested Status
Jürgen Gmach Approve
Review via email: mp+423399@code.launchpad.net

Commit message

This branch adds pre- and post- run hooks to jobs in lpcraft via the `run-before` and `run-after` configuration keys and the `lpcraft_execute_before_run` and `lpcraft_execute_after_run` hooks for plugins to configure.

It also includes a slight refactor of the `run_job` logic to avoid code duplication.

To post a comment you must log in.
Revision history for this message
Jürgen Gmach (jugmac00) wrote :

self-approving as the main parts were already improved and just a bit of documentation and changelog entries were added

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/NEWS.rst b/NEWS.rst
2index 4e89f28..bcec6ce 100644
3--- a/NEWS.rst
4+++ b/NEWS.rst
5@@ -5,7 +5,10 @@ Version history
6 0.0.15 (unreleased)
7 ===================
8
9-- Nothing yet.
10+- Allow ``run-before`` and ``run-after`` in ``.launchpad.yaml`` config.
11+
12+- Add ``lpcraft_execute_before_run`` and ``lpcraft_execute_after_run`` hooks.
13+
14
15 0.0.14 (2022-05-18)
16 ===================
17diff --git a/docs/configuration.rst b/docs/configuration.rst
18index 5d16337..5e02c2b 100644
19--- a/docs/configuration.rst
20+++ b/docs/configuration.rst
21@@ -60,10 +60,18 @@ Job definitions
22 ``plugin`` (optional)
23 A plugin which will be used for this job. See :doc:`../plugins`
24
25+``run-before`` (optional)
26+ A string (possibly multi-line) containing shell commands to run for this
27+ job prior to the main ``run`` section.
28+
29 ``run`` (optional)
30 A string (possibly multi-line) containing shell commands to run for this
31 job.
32
33+``run-after`` (optional)
34+ A string (possibly multi-line) containing shell commands to run for this
35+ job after the main ``run`` section.
36+
37 ``output`` (optional)
38 See the :ref:`output-properties` section below.
39
40diff --git a/lpcraft/commands/run.py b/lpcraft/commands/run.py
41index 106ac2f..961b198 100644
42--- a/lpcraft/commands/run.py
43+++ b/lpcraft/commands/run.py
44@@ -9,12 +9,13 @@ import os
45 import shlex
46 from argparse import Namespace
47 from pathlib import Path, PurePath
48-from typing import List, Optional, Set
49+from typing import Dict, List, Optional, Set
50
51 from craft_cli import EmitterMode, emit
52-from craft_providers import Executor
53+from craft_providers import Executor, lxd
54 from craft_providers.actions.snap_installer import install_from_store
55 from dotenv import dotenv_values
56+from pluggy import PluginManager
57
58 from lpcraft import env
59 from lpcraft.config import Config, Job, Output
60@@ -193,6 +194,108 @@ def _copy_output_properties(
61 json.dump(properties, f)
62
63
64+def _resolve_runtime_value(
65+ pm: PluginManager, job: Job, hook_name: str, job_property: str
66+) -> Optional[str]:
67+ command_from_config: Optional[str] = getattr(job, job_property, None)
68+ if command_from_config is not None:
69+ return command_from_config
70+ rv: List[str] = getattr(pm.hook, hook_name)()
71+ return next(iter(rv), None)
72+
73+
74+def _install_apt_packages(
75+ job_name: str,
76+ job: Job,
77+ packages: List[str],
78+ instance: lxd.LXDInstance,
79+ host_architecture: str,
80+ remote_cwd: Path,
81+ apt_replacement_repositories: Optional[List[str]],
82+ environment: Optional[Dict[str, Optional[str]]],
83+) -> None:
84+ if apt_replacement_repositories:
85+ # replace sources.list
86+ lines = "\n".join(apt_replacement_repositories) + "\n"
87+ with emit.open_stream("Replacing /etc/apt/sources.list") as stream:
88+ instance.push_file_io(
89+ destination=PurePath("/etc/apt/sources.list"),
90+ content=io.BytesIO(lines.encode()),
91+ file_mode="0644",
92+ group="root",
93+ user="root",
94+ )
95+ # update local repository information
96+ apt_update = ["apt", "update"]
97+ with emit.open_stream(f"Running {apt_update}") as stream:
98+ proc = instance.execute_run(
99+ apt_update,
100+ cwd=remote_cwd,
101+ env=environment,
102+ stdout=stream,
103+ stderr=stream,
104+ )
105+ if proc.returncode != 0:
106+ raise CommandError(
107+ f"Job {job_name!r} for "
108+ f"{job.series}/{host_architecture} failed with "
109+ f"exit status {proc.returncode} "
110+ f"while running `{shlex.join(apt_update)}`.",
111+ retcode=proc.returncode,
112+ )
113+ packages_cmd = ["apt", "install", "-y"] + packages
114+ emit.progress("Installing system packages")
115+ with emit.open_stream(f"Running {packages_cmd}") as stream:
116+ proc = instance.execute_run(
117+ packages_cmd,
118+ cwd=remote_cwd,
119+ env=environment,
120+ stdout=stream,
121+ stderr=stream,
122+ )
123+ if proc.returncode != 0:
124+ raise CommandError(
125+ f"Job {job_name!r} for "
126+ f"{job.series}/{host_architecture} failed with "
127+ f"exit status {proc.returncode} "
128+ f"while running `{shlex.join(packages_cmd)}`.",
129+ retcode=proc.returncode,
130+ )
131+
132+
133+def _run_instance_command(
134+ command: str,
135+ job_name: str,
136+ job: Job,
137+ instance: lxd.LXDInstance,
138+ host_architecture: str,
139+ remote_cwd: Path,
140+ environment: Optional[Dict[str, Optional[str]]],
141+) -> None:
142+ full_run_cmd = ["bash", "--noprofile", "--norc", "-ec", command]
143+ emit.progress("Running command for the job...")
144+ original_mode = emit.get_mode()
145+ if original_mode == EmitterMode.NORMAL:
146+ emit.set_mode(EmitterMode.VERBOSE)
147+ with emit.open_stream(f"Running {full_run_cmd}") as stream:
148+ proc = instance.execute_run(
149+ full_run_cmd,
150+ cwd=remote_cwd,
151+ env=environment,
152+ stdout=stream,
153+ stderr=stream,
154+ )
155+ if original_mode == EmitterMode.NORMAL:
156+ emit.set_mode(original_mode)
157+ if proc.returncode != 0:
158+ raise CommandError(
159+ f"Job {job_name!r} for "
160+ f"{job.series}/{host_architecture} failed with "
161+ f"exit status {proc.returncode}.",
162+ retcode=proc.returncode,
163+ )
164+
165+
166 def _run_job(
167 job_name: str,
168 job: Job,
169@@ -208,15 +311,24 @@ def _run_job(
170 if host_architecture not in job.architectures:
171 return
172 pm = get_plugin_manager(job)
173- # XXX jugmac00 2021-12-17: extract inferring run_command
174- run_command = None
175-
176- run_from_configuration = job.run
177- if run_from_configuration is not None:
178- run_command = run_from_configuration
179- else:
180- rv = pm.hook.lpcraft_execute_run()
181- run_command = rv and rv[0] or None
182+ pre_run_command = _resolve_runtime_value(
183+ pm,
184+ job,
185+ hook_name="lpcraft_execute_before_run",
186+ job_property="run_before",
187+ )
188+ run_command = _resolve_runtime_value(
189+ pm,
190+ job,
191+ hook_name="lpcraft_execute_run",
192+ job_property="run",
193+ )
194+ post_run_command = _resolve_runtime_value(
195+ pm,
196+ job,
197+ hook_name="lpcraft_execute_after_run",
198+ job_property="run_after",
199+ )
200
201 if not run_command:
202 raise CommandError(
203@@ -265,77 +377,27 @@ def _run_job(
204 )
205 packages = list(itertools.chain(*pm.hook.lpcraft_install_packages()))
206 if packages:
207- if apt_replacement_repositories:
208- # replace sources.list
209- lines = "\n".join(apt_replacement_repositories) + "\n"
210- with emit.open_stream(
211- "Replacing /etc/apt/sources.list"
212- ) as stream:
213- instance.push_file_io(
214- destination=PurePath("/etc/apt/sources.list"),
215- content=io.BytesIO(lines.encode()),
216- file_mode="0644",
217- group="root",
218- user="root",
219- )
220- # update local repository information
221- apt_update = ["apt", "update"]
222- with emit.open_stream(f"Running {apt_update}") as stream:
223- proc = instance.execute_run(
224- apt_update,
225- cwd=remote_cwd,
226- env=environment,
227- stdout=stream,
228- stderr=stream,
229- )
230- if proc.returncode != 0:
231- raise CommandError(
232- f"Job {job_name!r} for "
233- f"{job.series}/{host_architecture} failed with "
234- f"exit status {proc.returncode} "
235- f"while running `{shlex.join(apt_update)}`.",
236- retcode=proc.returncode,
237- )
238- packages_cmd = ["apt", "install", "-y"] + packages
239- emit.progress("Installing system packages")
240- with emit.open_stream(f"Running {packages_cmd}") as stream:
241- proc = instance.execute_run(
242- packages_cmd,
243- cwd=remote_cwd,
244- env=environment,
245- stdout=stream,
246- stderr=stream,
247- )
248- if proc.returncode != 0:
249- raise CommandError(
250- f"Job {job_name!r} for "
251- f"{job.series}/{host_architecture} failed with "
252- f"exit status {proc.returncode} "
253- f"while running `{shlex.join(packages_cmd)}`.",
254- retcode=proc.returncode,
255- )
256- full_run_cmd = ["bash", "--noprofile", "--norc", "-ec", run_command]
257- emit.progress("Running the job")
258- original_mode = emit.get_mode()
259- if original_mode == EmitterMode.NORMAL:
260- emit.set_mode(EmitterMode.VERBOSE)
261- with emit.open_stream(f"Running {full_run_cmd}") as stream:
262- proc = instance.execute_run(
263- full_run_cmd,
264- cwd=remote_cwd,
265- env=environment,
266- stdout=stream,
267- stderr=stream,
268- )
269- if original_mode == EmitterMode.NORMAL:
270- emit.set_mode(original_mode)
271- if proc.returncode != 0:
272- raise CommandError(
273- f"Job {job_name!r} for "
274- f"{job.series}/{host_architecture} failed with "
275- f"exit status {proc.returncode}.",
276- retcode=proc.returncode,
277+ _install_apt_packages(
278+ job_name=job_name,
279+ job=job,
280+ packages=packages,
281+ instance=instance,
282+ host_architecture=host_architecture,
283+ remote_cwd=remote_cwd,
284+ apt_replacement_repositories=apt_replacement_repositories,
285+ environment=environment,
286 )
287+ for cmd in (pre_run_command, run_command, post_run_command):
288+ if cmd:
289+ _run_instance_command(
290+ command=cmd,
291+ job_name=job_name,
292+ job=job,
293+ instance=instance,
294+ host_architecture=host_architecture,
295+ remote_cwd=remote_cwd,
296+ environment=environment,
297+ )
298
299 if job.output is not None and output is not None:
300 target_path = output / job_name / job.series / host_architecture
301@@ -401,6 +463,7 @@ def run(args: Namespace) -> int:
302 emit.error(e)
303 stage_failed = True
304 if stage_failed:
305+ # FIXME: should we still clean here?
306 raise CommandError(
307 f"Some jobs in {stage} failed; stopping.", retcode=1
308 )
309diff --git a/lpcraft/config.py b/lpcraft/config.py
310index 07f17ff..06f6e23 100644
311--- a/lpcraft/config.py
312+++ b/lpcraft/config.py
313@@ -67,7 +67,9 @@ class Job(ModelConfigDefaults):
314
315 series: _Identifier
316 architectures: List[_Identifier]
317+ run_before: Optional[StrictStr]
318 run: Optional[StrictStr]
319+ run_after: Optional[StrictStr]
320 environment: Optional[Dict[str, Optional[str]]]
321 output: Optional[Output]
322 snaps: Optional[List[StrictStr]]
323diff --git a/lpcraft/plugin/hookspecs.py b/lpcraft/plugin/hookspecs.py
324index 6b198ea..ddb3b8a 100644
325--- a/lpcraft/plugin/hookspecs.py
326+++ b/lpcraft/plugin/hookspecs.py
327@@ -7,6 +7,8 @@ __all__ = [
328 "lpcraft_install_packages",
329 "lpcraft_install_snaps",
330 "lpcraft_execute_run",
331+ "lpcraft_execute_before_run",
332+ "lpcraft_execute_after_run",
333 "lpcraft_set_environment",
334 ]
335
336@@ -41,3 +43,13 @@ def lpcraft_set_environment() -> dict[str, str | None]:
337 # Please note: when there is the same environment variable provided by
338 # the plugin and the configuration file, the one in the configuration
339 # file will be taken into account
340+
341+
342+@hookspec # type: ignore
343+def lpcraft_execute_before_run() -> str:
344+ """Command to execute prior to the main execution body."""
345+
346+
347+@hookspec # type: ignore
348+def lpcraft_execute_after_run() -> str:
349+ """Command to execute after the main execution body."""
350diff --git a/lpcraft/tests/test_config.py b/lpcraft/tests/test_config.py
351index 6867024..d34b718 100644
352--- a/lpcraft/tests/test_config.py
353+++ b/lpcraft/tests/test_config.py
354@@ -59,8 +59,10 @@ class TestConfig(TestCase):
355 test:
356 series: focal
357 architectures: [amd64, arm64]
358+ run-before: pip install --upgrade setuptools build
359 run: |
360 tox
361+ run-after: coverage report
362 """
363 )
364 )
365@@ -76,7 +78,9 @@ class TestConfig(TestCase):
366 MatchesStructure.byEquality(
367 series="focal",
368 architectures=["amd64", "arm64"],
369+ run_before="pip install --upgrade setuptools build", # noqa:E501
370 run="tox\n",
371+ run_after="coverage report",
372 )
373 ]
374 )

Subscribers

People subscribed via source and target branches