Coverage for src/debputy/build_support/build_logic.py: 11%
141 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-10-12 15:06 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-10-12 15:06 +0000
1import collections
2import contextlib
3import os
4import tempfile
5from typing import (
6 List,
7 Dict,
8 Optional,
9 TYPE_CHECKING,
10 Tuple,
11 FrozenSet,
12)
13from collections.abc import Iterator, Mapping
15from debputy.build_support.build_context import BuildContext
16from debputy.build_support.buildsystem_detection import (
17 auto_detect_buildsystem,
18)
19from debputy.commands.debputy_cmd.context import CommandContext
20from debputy.manifest_parser.base_types import BuildEnvironmentDefinition
21from debputy.packages import BinaryPackage
22from debputy.plugins.debputy.to_be_api_types import BuildRule
23from debputy.util import (
24 _error,
25 _info,
26 _non_verbose_info,
27 generated_content_dir,
28)
30if TYPE_CHECKING:
31 from debputy.highlevel_manifest import HighLevelManifest
34@contextlib.contextmanager
35def in_build_env(
36 build_env: BuildEnvironmentDefinition,
37 *,
38 env_is_for_clean: bool = False,
39) -> Iterator[None]:
40 # Should possibly be per build
41 with _setup_build_env(build_env, env_is_for_clean=env_is_for_clean):
42 yield
45def _set_stem_if_absent(stems: list[str | None], idx: int, stem: str) -> None:
46 if stems[idx] is None:
47 stems[idx] = stem
50def assign_stems(
51 build_rules: list[BuildRule],
52 manifest: "HighLevelManifest",
53) -> None:
54 if not build_rules:
55 return
56 if len(build_rules) == 1:
57 build_rules[0].auto_generated_stem = ""
58 return
60 debs = {p.name for p in manifest.all_packages if p.package_type == "deb"}
61 udebs = {p.name for p in manifest.all_packages if p.package_type == "udeb"}
62 deb_only_builds: list[int] = []
63 udeb_only_builds: list[int] = []
64 by_name_only_builds: dict[str, list[int]] = collections.defaultdict(list)
65 stems = [rule.name for rule in build_rules]
66 reserved_stems = {n for n in stems if n is not None}
68 for idx, rule in enumerate(build_rules):
69 stem = stems[idx]
70 if stem is not None:
71 continue
72 pkg_names = {p.name for p in rule.for_packages}
73 if pkg_names == debs:
74 deb_only_builds.append(idx)
75 elif pkg_names == udebs:
76 udeb_only_builds.append(idx)
78 if len(pkg_names) == 1:
79 pkg_name = next(iter(pkg_names))
80 by_name_only_builds[pkg_name].append(idx)
82 if "deb" not in reserved_stems and len(deb_only_builds) == 1:
83 _set_stem_if_absent(stems, deb_only_builds[0], "deb")
85 if "udeb" not in reserved_stems and len(udeb_only_builds) == 1:
86 _set_stem_if_absent(stems, udeb_only_builds[0], "udeb")
88 for pkg, idxs in by_name_only_builds.items():
89 if len(idxs) != 1 or pkg in reserved_stems:
90 continue
91 _set_stem_if_absent(stems, idxs[0], pkg)
93 for idx, rule in enumerate(build_rules):
94 stem = stems[idx]
95 if stem is None:
96 stem = f"bno_{idx}"
97 rule.auto_generated_stem = stem
98 _info(f"Assigned {rule.auto_generated_stem} [{stem}] to step {idx}")
101def perform_builds(
102 context: CommandContext,
103 manifest: "HighLevelManifest",
104 build_system_install_dirs: list[tuple[str, frozenset[BinaryPackage]]],
105) -> None:
106 prune_unnecessary_env()
107 build_rules = manifest.build_rules
108 if build_rules is not None:
109 if not build_rules:
110 # Defined but empty disables the auto-detected build system
111 return
112 active_packages = frozenset(manifest.active_packages)
113 condition_context = manifest.source_condition_context
114 build_context = BuildContext.from_command_context(context)
115 assign_stems(build_rules, manifest)
116 for step_no, build_rule in enumerate(build_rules):
117 step_ref = (
118 f"step {step_no} [{build_rule.auto_generated_stem}]"
119 if build_rule.name is None
120 else f"step {step_no} [{build_rule.name}]"
121 )
122 if build_rule.for_packages.isdisjoint(active_packages):
123 _info(
124 f"Skipping build for {step_ref}: None of the relevant packages are being built"
125 )
126 continue
127 manifest_condition = build_rule.manifest_condition
128 if manifest_condition is not None and not manifest_condition.evaluate(
129 condition_context
130 ):
131 _info(
132 f"Skipping build for {step_ref}: The condition clause evaluated to false"
133 )
134 continue
135 _info(f"Starting build for {step_ref}.")
136 with in_build_env(build_rule.environment):
137 try:
138 build_rule.run_build(build_context, manifest)
139 except (RuntimeError, AttributeError) as e:
140 if context.parsed_args.debug_mode:
141 raise e
142 _error(
143 f"An error occurred during build/install at {step_ref} (defined at {build_rule.attribute_path.path}): {str(e)}"
144 )
145 dest_dir = build_rule.install_dest_dir()
146 if dest_dir is not None:
147 build_system_install_dirs.append((dest_dir, build_rule.for_packages))
149 _info(f"Completed build for {step_ref}.")
151 else:
152 build_system = auto_detect_buildsystem(manifest)
153 if build_system:
154 _info(f"Auto-detected build system: {build_system.__class__.__name__}")
155 build_context = BuildContext.from_command_context(context)
156 with in_build_env(build_system.environment):
157 build_system.run_build(
158 build_context,
159 manifest,
160 )
161 dest_dir = build_system.install_dest_dir()
162 if dest_dir is not None:
163 build_system_install_dirs.append(
164 (dest_dir, build_system.for_packages)
165 )
167 _non_verbose_info("Upstream builds completed successfully")
168 else:
169 _info("No build system was detected from the current plugin set.")
172def prune_unnecessary_env() -> None:
173 vs = [
174 "XDG_CACHE_HOME",
175 "XDG_CONFIG_DIRS",
176 "XDG_CONFIG_HOME",
177 "XDG_DATA_HOME",
178 "XDG_DATA_DIRS",
179 "XDG_RUNTIME_DIR",
180 ]
181 for v in vs:
182 if v in os.environ:
183 del os.environ[v]
184 os.environ["HOME"] = "/non-existent"
187@contextlib.contextmanager
188def _setup_build_env(
189 build_env: BuildEnvironmentDefinition,
190 *,
191 env_is_for_clean: bool = False,
192) -> Iterator[None]:
193 env_backup = dict(os.environ)
194 env = dict(env_backup)
195 had_delta = False
196 build_env.update_env(env)
197 if env != env_backup:
198 _set_env(env)
199 had_delta = True
200 _info("Updated environment to match build")
201 if env_is_for_clean:
202 yield
203 else:
204 orig_home = env_backup["HOME"]
205 env["HOME"] = generated_content_dir(subdir_key="dpty_buildtime_home")
206 with tempfile.TemporaryDirectory() as xdg_runtime_dir:
207 env["XDG_RUNTIME_DIR"] = xdg_runtime_dir
208 yield
209 try:
210 del env["XDG_RUNTIME_DIR"]
211 except KeyError:
212 pass
213 env["HOME"] = orig_home
214 if had_delta or env != env_backup:
215 _set_env(env_backup)
218def _set_env(desired_env: Mapping[str, str]) -> None:
219 os_env = os.environ
220 for key in os_env.keys() | desired_env.keys():
221 desired_value = desired_env.get(key)
222 if desired_value is None:
223 try:
224 del os_env[key]
225 except KeyError:
226 pass
227 else:
228 os_env[key] = desired_value