Coverage for src/debputy/plugins/debputy/build_system_rules.py: 33%
715 statements
« prev ^ index » next coverage.py v7.8.2, created at 2026-07-22 10:58 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2026-07-22 10:58 +0000
1import collections
2import dataclasses
3import json
4import os
5import shutil
6import subprocess
7import textwrap
8import typing
9from typing import (
10 NotRequired,
11 TypedDict,
12 Self,
13 cast,
14 Union,
15 TYPE_CHECKING,
16 Literal,
17 Required,
18)
19from collections.abc import Mapping, Sequence, MutableMapping, Iterable, Container
21from debian.debian_support import Version
23from debputy._manifest_constants import MK_BUILDS
24from debputy.architecture_support import DpkgArchitectureBuildProcessValuesTable
25from debputy.manifest_conditions import ManifestCondition
26from debputy.manifest_parser.base_types import (
27 BuildEnvironmentDefinition,
28 DebputyParsedContentStandardConditional,
29 FileSystemExactMatchRule,
30)
31from debputy.manifest_parser.exceptions import (
32 ManifestParseException,
33 ManifestInvalidUserDataException,
34)
35from debputy.manifest_parser.parser_data import ParserContextData
36from debputy.manifest_parser.tagging_types import DebputyParsedContent
37from debputy.manifest_parser.util import AttributePath
38from debputy.plugin.api import reference_documentation
39from debputy.plugin.api.impl import (
40 DebputyPluginInitializerProvider,
41)
42from debputy.plugin.api.parser_tables import OPARSER_MANIFEST_ROOT
43from debputy.plugin.api.spec import (
44 documented_attr,
45 INTEGRATION_MODE_FULL,
46 only_integrations,
47 VirtualPath,
48)
49from debputy.plugin.api.std_docs import docs_from
50from debputy.plugins.debputy.to_be_api_types import (
51 BuildRule,
52 StepBasedBuildSystemRule,
53 OptionalInstallDirectly,
54 BuildSystemCharacteristics,
55 OptionalBuildDirectory,
56 OptionalInSourceBuild,
57 MakefileSupport,
58 BuildRuleParsedFormat,
59 debputy_build_system,
60 CleanHelper,
61 NinjaBuildSupport,
62 TestRule,
63 OptionalTestRule,
64)
65from debputy.types import EnvironmentModification
66from debputy.util import (
67 _warn,
68 run_build_system_command,
69 _error,
70 PerlConfigVars,
71 resolve_perl_config,
72 generated_content_dir,
73 manifest_format_doc,
74 _is_debug_log_enabled,
75 _info,
76)
78if TYPE_CHECKING:
79 from debputy.build_support.build_context import BuildContext
80 from debputy.highlevel_manifest import HighLevelManifest
83PERL_CMD = "perl"
85TOOL_VARIABLES = {
86 "CC": "gcc",
87 "CXX": "g++",
88 "PKG_CONFIG": "pkg-config",
89 "QMAKE": "qmake",
90}
93@typing.overload
94def find_source_level_build_tool( 94 ↛ exitline 94 didn't return from function 'find_source_level_build_tool' because
95 dpkg_architecture_variables: DpkgArchitectureBuildProcessValuesTable,
96 *,
97 command: str = "",
98 tool_variable: str = "",
99 resolve: bool = False,
100 required: typing.Literal[False] = False,
101 native: bool = False,
102) -> str | None: ...
105@typing.overload
106def find_source_level_build_tool( 106 ↛ exitline 106 didn't return from function 'find_source_level_build_tool' because
107 dpkg_architecture_variables: DpkgArchitectureBuildProcessValuesTable,
108 *,
109 command: str = "",
110 tool_variable: str = "",
111 resolve: bool = False,
112 required: typing.Literal[True] = True,
113 native: bool = False,
114) -> str: ...
117def find_source_level_build_tool(
118 dpkg_architecture_variables: DpkgArchitectureBuildProcessValuesTable,
119 *,
120 command: str = "",
121 tool_variable: str = "",
122 resolve: bool = False,
123 required: bool = False,
124 native: bool = False,
125) -> str | None:
126 cmd: str | None
127 if tool_variable:
128 var = f"{tool_variable}" if native else tool_variable
129 cmd = os.environ.get(var)
130 if cmd:
131 _info(f"Detected build tool {cmd} from environment {var}")
132 if not resolve and not required:
133 return cmd
134 resolved = shutil.which(cmd)
135 if not resolved:
136 if required:
137 _error(
138 f"Command {cmd} not found as resolved from environment variable {var}"
139 )
140 _warn(
141 f"Environment variable {tool_variable} points to non-existent tool {cmd}. Ignoring"
142 )
143 return None
144 return resolved if resolve else cmd
146 try:
147 cmd = TOOL_VARIABLES[tool_variable]
148 except KeyError as e:
149 raise ValueError(f"Unknown/Unsupported variable {tool_variable}") from e
150 else:
151 cmd = command
153 if not cmd:
154 raise ValueError(
155 "At least one of 'tool_variable' and 'command' must be passed and non-empty"
156 )
158 arch_part = "BUILD" if native else "HOST"
159 deb_tuple = dpkg_architecture_variables[f"DEB_{arch_part}_GNU_TYPE"]
160 prefixed_cmd = f"{deb_tuple}-{cmd}"
161 resolved = shutil.which(prefixed_cmd)
162 if resolved:
163 return resolved if resolve else prefixed_cmd
164 if dpkg_architecture_variables.is_cross_compiling and not native:
165 if required:
166 _error(f"Command {prefixed_cmd} not found.")
167 return None
168 # Falling back to the unprefixed version of the tool.
169 resolved = shutil.which(cmd)
170 if resolved:
171 return resolved if resolve else prefixed_cmd
172 if not required:
173 return None
174 _error(
175 f"Neither the command {prefixed_cmd} nor {cmd} (its fallback command) were found."
176 )
179class Conditional(DebputyParsedContent):
180 when: Required[ManifestCondition]
183def register_build_system_rules(api: DebputyPluginInitializerProvider) -> None:
184 register_build_keywords(api)
185 register_build_rules(api)
188def register_build_keywords(api: DebputyPluginInitializerProvider) -> None:
190 api.pluggable_manifest_rule(
191 OPARSER_MANIFEST_ROOT,
192 "build-environments",
193 list[NamedEnvironmentSourceFormat],
194 _parse_build_environments,
195 register_value=False,
196 expected_debputy_integration_mode=only_integrations(INTEGRATION_MODE_FULL),
197 inline_reference_documentation=reference_documentation(
198 title="Build Environments (`build-environments`)",
199 description=textwrap.dedent("""\
200 Define named environments to set the environment for any build commands that needs
201 a non-default environment.
203 The environment definitions can be used to tweak the environment variables used by the
204 build commands. An example:
206 build-environments:
207 - name: custom-env
208 set:
209 ENV_VAR: foo
210 ANOTHER_ENV_VAR: bar
211 builds:
212 - autoconf:
213 environment: custom-env
215 The environment definition has multiple attributes for setting environment variables
216 which determines when the definition is applied. The resulting environment is the
217 result of the following order of operations.
219 1. The environment `debputy` received from its parent process.
220 2. Apply all the variable definitions from `set` (if the attribute is present)
221 3. Apply all computed variables (such as variables from `dpkg-buildflags`).
222 4. Apply all the variable definitions from `override` (if the attribute is present)
223 5. Remove all variables listed in `unset` (if the attribute is present).
225 Accordingly, both `override` and `unset` will overrule any computed variables while
226 `set` will be overruled by any computed variables.
228 Note that these variables are not available via manifest substitution. They are
229 only available and visible to build commands.
230 """),
231 attributes=[
232 documented_attr(
233 "name",
234 textwrap.dedent("""\
235 The name of the environment
237 The name is used to reference the environment from build rules.
238 """),
239 ),
240 documented_attr(
241 "set",
242 textwrap.dedent("""\
243 A mapping of environment variables to be set.
245 Note these environment variables are set before computed variables (such
246 as `dpkg-buildflags`) are provided. They can affect the content of the
247 computed variables, but they cannot overrule them. If you need to overrule
248 a computed variable, please use `override` instead.
249 """),
250 ),
251 documented_attr(
252 "override",
253 textwrap.dedent("""\
254 A mapping of environment variables to set.
256 Similar to `set`, but it can overrule computed variables like those from
257 `dpkg-buildflags`.
258 """),
259 ),
260 documented_attr(
261 "unset",
262 textwrap.dedent("""\
263 A list of environment variables to unset.
265 Any environment variable named here will be unset. No warnings or errors
266 will be raised if a given variable was not set.
267 """),
268 ),
269 ],
270 reference_documentation_url=manifest_format_doc(
271 "build-environment-build-environment"
272 ),
273 ),
274 )
275 api.pluggable_manifest_rule(
276 OPARSER_MANIFEST_ROOT,
277 "default-build-environment",
278 EnvironmentSourceFormat,
279 _parse_default_environment,
280 register_value=False,
281 expected_debputy_integration_mode=only_integrations(INTEGRATION_MODE_FULL),
282 inline_reference_documentation=reference_documentation(
283 title="Default Build Environment (`default-build-environment`)",
284 description=textwrap.dedent("""\
285 Define the environment variables used in all build commands that uses the default
286 environment.
288 The environment definition can be used to tweak the environment variables used by the
289 build commands. An example:
291 default-build-environment:
292 set:
293 ENV_VAR: foo
294 ANOTHER_ENV_VAR: bar
296 The environment definition has multiple attributes for setting environment variables
297 which determines when the definition is applied. The resulting environment is the
298 result of the following order of operations.
300 1. The environment `debputy` received from its parent process.
301 2. Apply all the variable definitions from `set` (if the attribute is present)
302 3. Apply all computed variables (such as variables from `dpkg-buildflags`).
303 4. Apply all the variable definitions from `override` (if the attribute is present)
304 5. Remove all variables listed in `unset` (if the attribute is present).
306 Accordingly, both `override` and `unset` will overrule any computed variables while
307 `set` will be overruled by any computed variables.
309 Note that these variables are not available via manifest substitution. They are
310 only available and visible to build commands.
311 """),
312 attributes=[
313 documented_attr(
314 "set",
315 textwrap.dedent("""\
316 A mapping of environment variables to be set.
318 Note these environment variables are set before computed variables (such
319 as `dpkg-buildflags`) are provided. They can affect the content of the
320 computed variables, but they cannot overrule them. If you need to overrule
321 a computed variable, please use `override` instead.
322 """),
323 ),
324 documented_attr(
325 "override",
326 textwrap.dedent("""\
327 A mapping of environment variables to set.
329 Similar to `set`, but it can overrule computed variables like those from
330 `dpkg-buildflags`.
331 """),
332 ),
333 documented_attr(
334 "unset",
335 textwrap.dedent("""\
336 A list of environment variables to unset.
338 Any environment variable named here will be unset. No warnings or errors
339 will be raised if a given variable was not set.
340 """),
341 ),
342 ],
343 reference_documentation_url=manifest_format_doc(
344 "build-environment-build-environment"
345 ),
346 ),
347 )
348 api.pluggable_manifest_rule(
349 OPARSER_MANIFEST_ROOT,
350 MK_BUILDS,
351 list[BuildRule],
352 _handle_build_rules,
353 register_value=False,
354 expected_debputy_integration_mode=only_integrations(INTEGRATION_MODE_FULL),
355 inline_reference_documentation=reference_documentation(
356 title=f"Build rules (`{MK_BUILDS}`)",
357 description=textwrap.dedent("""\
358 Define how to build the upstream part of the package. Usually this is done via "build systems",
359 which also defines the clean rules.
361 A simple example is:
363 ```yaml
364 builds:
365 - autoconf:
366 configure-args:
367 - "--enable-foo"
368 - "--without=bar"
369 ```
371 Multiple builds are supported out of box. Here an example of how the build rules
372 for `libpam-krb5` might look.
374 ```yaml
375 builds:
376 - autoconf:
377 for: libpam-krb5
378 install-directly-to-package: true
379 configure-args:
380 - "--enable-reduced-depends"
381 - "--with-krb5-include=/usr/include/mit-krb5"
382 - "--with-krb5-lib=/usr/lib/{{DEB_HOST_MULTIARCH}}/mit-krb5"
383 - "--with-kadm-client-include=/usr/include/mit-krb5"
384 - "--with-kadm-client-lib=/usr/lib/{{DEB_HOST_MULTIARCH}}/mit-krb5"
385 - autoconf:
386 for: libpam-heimdal
387 install-directly-to-package: true
388 configure-args:
389 - "--enable-reduced-depends"
390 - "--with-krb5-include=/usr/include/heimdal"
391 - "--with-krb5-lib=/usr/lib/{{DEB_HOST_MULTIARCH}}/heimdal"
392 - "--with-kadm-client-include=/usr/include/heimdal"
393 - "--with-kadm-client-lib=/usr/lib/{{DEB_HOST_MULTIARCH}}/heimdal"
394 ```
395 """),
396 ),
397 )
399 api.provide_manifest_keyword(
400 TestRule,
401 "skip-tests",
402 lambda n, ap, pc: TestRule(
403 ap.path,
404 ManifestCondition.literal_bool(False),
405 ManifestCondition.literal_bool(True),
406 ),
407 inline_reference_documentation=reference_documentation(
408 title="Skip build time tests (`skip-tests`)",
409 description=textwrap.dedent("""\
410 Skip all build time tests.
412 Example:
414 ```yaml
415 ...:
416 # The tests require internet access and cannot be run at build time.
417 test-rule: skip-tests
418 ```
420 """),
421 ),
422 )
424 api.pluggable_manifest_rule(
425 TestRule,
426 "skip-tests-when",
427 Conditional,
428 lambda n, pf, ap, pc: TestRule(
429 ap.path,
430 pf["when"].negated(),
431 ManifestCondition.literal_bool(False),
432 ),
433 source_format=ManifestCondition,
434 inline_reference_documentation=reference_documentation(
435 title="Conditionally skip build time tests (`skip-tests-when`)",
436 description=textwrap.dedent("""\
437 The `skip-tests-when` test rule accepts a conditional. When that conditional evaluates to
438 true, the build time tests will be skipped. Otherwise, they will be run as usual.
440 Note if you want to only run the tests when the conditional evaluates to `true`,
441 then wrap the conditional with `not:`.
443 Example:
445 ```yaml
446 ...:
447 # Only run for linux-any (`DEB_HOST_ARCH`).
448 test-rule:
449 skip-tests-when:
450 not:
451 arch-matches: "linux-any"
452 ```
454 """),
455 ),
456 )
459def register_build_rules(api: DebputyPluginInitializerProvider) -> None:
460 api.register_build_system(ParsedAutoconfBuildRuleDefinition)
461 api.register_build_system(ParsedMakeBuildRuleDefinition)
463 api.register_build_system(ParsedPerlBuildBuildRuleDefinition)
464 api.register_build_system(ParsedPerlMakeMakerBuildRuleDefinition)
465 api.register_build_system(ParsedDebhelperBuildRuleDefinition)
467 api.register_build_system(ParsedCMakeBuildRuleDefinition)
468 api.register_build_system(ParsedMesonBuildRuleDefinition)
470 api.register_build_system(ParsedQmakeBuildRuleDefinition)
471 api.register_build_system(ParsedQmake6BuildRuleDefinition)
474class EnvironmentSourceFormat(TypedDict):
475 set: NotRequired[dict[str, str]]
476 override: NotRequired[dict[str, str]]
477 unset: NotRequired[list[str]]
480class NamedEnvironmentSourceFormat(EnvironmentSourceFormat):
481 name: str
484_READ_ONLY_ENV_VARS = {
485 "DEB_CHECK_COMMAND": None,
486 "DEB_SIGN_KEYID": None,
487 "DEB_SIGN_KEYFILE": None,
488 "DEB_BUILD_OPTIONS": "DEB_BUILD_MAINT_OPTIONS",
489 "DEB_BUILD_PROFILES": None,
490 "DEB_RULES_REQUIRES_ROOT": None,
491 "DEB_GAIN_ROOT_COMMAND": None,
492 "DH_EXTRA_ADDONS": None,
493 "DH_NO_ACT": None,
494 "XDG_RUNTIME_DIR": None,
495 "HOME": None,
496}
499def _check_variables(
500 env_vars: Iterable[str],
501 attribute_path: AttributePath,
502) -> None:
503 for env_var in env_vars:
504 if env_var not in _READ_ONLY_ENV_VARS: 504 ↛ 506line 504 didn't jump to line 506 because the condition on line 504 was always true
505 continue
506 alt = _READ_ONLY_ENV_VARS.get(env_var)
507 var_path = attribute_path[env_var].path_key_lc
508 if alt is None:
509 raise ManifestParseException(
510 f"The variable {env_var} cannot be modified by the manifest. This restriction is generally"
511 f" because the build should not touch those variables or changing them have no effect"
512 f" (since the consumer will not see the change). The problematic definition was {var_path}"
513 )
514 else:
515 raise ManifestParseException(
516 f"The variable {env_var} cannot be modified by the manifest. This restriction is generally"
517 f" because the build should not touch those variables or changing them have no effect"
518 f" (since the consumer will not see the change). Depending on what you are trying to"
519 f' accomplish, the variable "{alt}" might be a suitable alternative.'
520 f" The problematic definition was {var_path}"
521 )
524def _no_overlap(
525 lhs: Iterable[str | tuple[int, str]],
526 rhs: Container[str],
527 lhs_key: str,
528 rhs_key: str,
529 redundant_key: str,
530 attribute_path: AttributePath,
531) -> None:
532 for kt in lhs: 532 ↛ 533line 532 didn't jump to line 533 because the loop on line 532 never started
533 lhs_path_key, var = kt if isinstance(kt, tuple) else (kt, kt)
534 if var not in rhs:
535 continue
536 lhs_path = attribute_path[lhs_key][lhs_path_key].path_key_lc
537 rhs_path = attribute_path[rhs_key][var].path_key_lc
538 r_path = lhs_path if redundant_key == rhs_key else rhs_path
539 raise ManifestParseException(
540 f"The environment variable {var} was declared in {lhs_path} and {rhs_path}."
541 f" Due to how the variables are applied, the definition in {r_path} is redundant"
542 f" and can effectively be removed. Please review the manifest and remove one of"
543 f" the two definitions."
544 )
547@dataclasses.dataclass(slots=True, frozen=True)
548class ManifestProvidedBuildEnvironment(BuildEnvironmentDefinition):
550 name: str
551 is_default: bool
552 attribute_path: AttributePath
553 parser_context: ParserContextData
555 set_vars: Mapping[str, str]
556 override_vars: Mapping[str, str]
557 unset_vars: Sequence[str]
559 @classmethod
560 def from_environment_definition(
561 cls,
562 env: EnvironmentSourceFormat,
563 attribute_path: AttributePath,
564 parser_context: ParserContextData,
565 is_default: bool = False,
566 ) -> Self:
567 reference_name: str | None
568 if is_default:
569 name = "default-env"
570 reference_name = None
571 else:
572 named_env = cast("NamedEnvironmentSourceFormat", env)
573 name = named_env["name"]
574 reference_name = name
576 set_vars = env.get("set", {})
577 override_vars = env.get("override", {})
578 unset_vars = env.get("unset", [])
579 _check_variables(set_vars, attribute_path["set"])
580 _check_variables(override_vars, attribute_path["override"])
581 _check_variables(unset_vars, attribute_path["unset"])
583 if not set_vars and not override_vars and not unset_vars: 583 ↛ 584line 583 didn't jump to line 584 because the condition on line 583 was never true
584 raise ManifestParseException(
585 f"The environment definition {attribute_path.path_key_lc} was empty. Please provide"
586 " some content or delete the definition."
587 )
589 _no_overlap(
590 enumerate(unset_vars),
591 set_vars,
592 "unset",
593 "set",
594 "set",
595 attribute_path,
596 )
597 _no_overlap(
598 enumerate(unset_vars),
599 override_vars,
600 "unset",
601 "override",
602 "override",
603 attribute_path,
604 )
605 _no_overlap(
606 override_vars,
607 set_vars,
608 "override",
609 "set",
610 "set",
611 attribute_path,
612 )
614 r = cls(
615 name,
616 is_default,
617 attribute_path,
618 parser_context,
619 set_vars,
620 override_vars,
621 unset_vars,
622 )
623 parser_context._register_build_environment(
624 reference_name,
625 r,
626 attribute_path,
627 is_default,
628 )
630 return r
632 def update_env(self, env: MutableMapping[str, str]) -> None:
633 if set_vars := self.set_vars: 633 ↛ 635line 633 didn't jump to line 635 because the condition on line 633 was always true
634 env.update(set_vars)
635 dpkg_env = self.dpkg_buildflags_env(env, self.attribute_path.path_key_lc)
636 if _is_debug_log_enabled(): 636 ↛ 637line 636 didn't jump to line 637 because the condition on line 636 was never true
637 self.log_computed_env(f"dpkg-buildflags [{self.name}]", dpkg_env)
638 if overlapping_env := dpkg_env.keys() & set_vars.keys(): 638 ↛ 639line 638 didn't jump to line 639 because the condition on line 638 was never true
639 for var in overlapping_env:
640 key_lc = self.attribute_path["set"][var].path_key_lc
641 _warn(
642 f'The variable "{var}" defined at {key_lc} is shadowed by a computed variable.'
643 f" If the manifest definition is more important, please define it via `override` rather than"
644 f" `set`."
645 )
646 env.update(dpkg_env)
647 if override_vars := self.override_vars: 647 ↛ 648line 647 didn't jump to line 648 because the condition on line 647 was never true
648 env.update(override_vars)
649 if unset_vars := self.unset_vars: 649 ↛ 650line 649 didn't jump to line 650 because the condition on line 649 was never true
650 for var in unset_vars:
651 try:
652 del env[var]
653 except KeyError:
654 pass
657_MAKE_DEFAULT_TOOLS = [
658 "CC",
659 "CXX",
660 "PKG_CONFIG",
661]
664class MakefileBuildSystemRule(StepBasedBuildSystemRule):
666 __slots__ = ("_make_support", "_build_target", "_install_target", "_directory")
668 def __init__(
669 self,
670 attributes: "ParsedMakeBuildRuleDefinition",
671 attribute_path: AttributePath,
672 parser_context: Union[ParserContextData, "HighLevelManifest"],
673 ) -> None:
674 super().__init__(attributes, attribute_path, parser_context)
675 directory = attributes.get("directory")
676 self._directory = directory.match_rule.path if directory else None
677 self._make_support = MakefileSupport.from_build_system(self)
678 self._build_target = attributes.get("build_target")
679 self._test_target = attributes.get("test_target")
680 self._install_target = attributes.get("install_target")
681 self._install_target_variables = self._unpack_macros(
682 "install_target_variables",
683 attributes,
684 attribute_path,
685 )
687 def _unpack_macros(
688 self,
689 key: str,
690 attributes: "ParsedMakeBuildRuleDefinition",
691 attribute_path: AttributePath,
692 ) -> dict[str, str]:
693 marcos: dict[str, str] | None = attributes.get(key)
694 if not marcos:
695 return {}
696 macro_container_path = attribute_path[key]
697 for k in marcos:
698 if "=" in k:
699 key_path = macro_container_path[k].path_key_lc
700 raise ManifestParseException(
701 f"The make variable {k} (at {key_path}) contains a '='. Such variable names are not supported."
702 )
703 return marcos
705 @classmethod
706 def auto_detect_build_system(
707 cls,
708 source_root: VirtualPath,
709 *args,
710 **kwargs,
711 ) -> bool:
712 return any(p in source_root for p in ("Makefile", "makefile", "GNUmakefile"))
714 @classmethod
715 def characteristics(cls) -> BuildSystemCharacteristics:
716 return BuildSystemCharacteristics(
717 out_of_source_builds="not-supported",
718 )
720 def configure_impl(
721 self,
722 context: "BuildContext",
723 manifest: "HighLevelManifest",
724 **kwargs,
725 ) -> None:
726 # No configure step
727 pass
729 def build_impl(
730 self,
731 context: "BuildContext",
732 manifest: "HighLevelManifest",
733 **kwargs,
734 ) -> None:
735 extra_vars = []
736 build_target = self._build_target
737 if build_target is not None:
738 extra_vars.append(build_target)
739 for envvar in _MAKE_DEFAULT_TOOLS:
740 tool = find_source_level_build_tool(
741 context.dpkg_architecture_variables,
742 tool_variable=envvar,
743 )
744 if tool:
745 extra_vars.append(f"{envvar}={tool}")
746 self._make_support.run_make(
747 context,
748 *extra_vars,
749 "INSTALL=install --strip-program=true",
750 directory=self._directory,
751 )
753 def test_impl(
754 self,
755 context: "BuildContext",
756 manifest: "HighLevelManifest",
757 *,
758 should_ignore_test_errors: bool = False,
759 **kwargs,
760 ) -> None:
761 self._run_make_maybe_explicit_target(
762 context,
763 self._test_target,
764 ["test", "check"],
765 )
767 def install_impl(
768 self,
769 context: "BuildContext",
770 manifest: "HighLevelManifest",
771 dest_dir: str,
772 **kwargs,
773 ) -> None:
774 variables = self._install_target_variables.copy()
775 for k, v in [
776 ("DESTDIR", dest_dir),
777 ("AM_UPDATE_INFO_DIR", "no"),
778 ("INSTALL", "install --strip-program=true"),
779 ]:
780 if k not in variables:
781 variables[k] = v
782 var_list = sorted(f"{k}={v}" for k, v in variables.items() if v is not None)
783 self._run_make_maybe_explicit_target(
784 context,
785 self._install_target,
786 ["install"],
787 *var_list,
788 )
790 def _run_make_maybe_explicit_target(
791 self,
792 context: "BuildContext",
793 provided_target: str | None,
794 fallback_targets: Sequence[str],
795 *make_args: str,
796 ) -> None:
797 make_support = self._make_support
798 if provided_target is not None:
799 make_support.run_make(
800 context,
801 provided_target,
802 *make_args,
803 directory=self._directory,
804 )
805 else:
806 make_support.run_first_existing_target_if_any(
807 context,
808 fallback_targets,
809 *make_args,
810 directory=self._directory,
811 )
813 def clean_impl(
814 self,
815 context: "BuildContext",
816 manifest: "HighLevelManifest",
817 clean_helper: "CleanHelper",
818 **kwargs,
819 ) -> None:
820 self._make_support.run_first_existing_target_if_any(
821 context,
822 ["distclean", "realclean", "clean"],
823 )
826class PerlBuildBuildSystemRule(StepBasedBuildSystemRule):
828 __slots__ = "configure_args"
830 def __init__(
831 self,
832 attributes: "ParsedPerlBuildBuildRuleDefinition",
833 attribute_path: AttributePath,
834 parser_context: Union[ParserContextData, "HighLevelManifest"],
835 ) -> None:
836 super().__init__(attributes, attribute_path, parser_context)
837 self.configure_args = attributes.get("configure_args", [])
839 @classmethod
840 def auto_detect_build_system(
841 cls,
842 source_root: VirtualPath,
843 *args,
844 **kwargs,
845 ) -> bool:
846 return "Build.PL" in source_root
848 @classmethod
849 def characteristics(cls) -> BuildSystemCharacteristics:
850 return BuildSystemCharacteristics(
851 out_of_source_builds="not-supported",
852 )
854 @staticmethod
855 def _perl_cross_build_env(
856 context: "BuildContext",
857 ) -> tuple[PerlConfigVars, EnvironmentModification | None]:
858 perl_config_data = resolve_perl_config(
859 context.dpkg_architecture_variables,
860 None,
861 )
862 if context.is_cross_compiling:
863 perl5lib_dir = perl_config_data.cross_inc_dir
864 if perl5lib_dir is not None:
865 env_perl5lib = os.environ.get("PERL5LIB")
866 if env_perl5lib is not None:
867 perl5lib_dir = (
868 perl5lib_dir + perl_config_data.path_sep + env_perl5lib
869 )
870 env_mod = EnvironmentModification(
871 replacements=(("PERL5LIB", perl5lib_dir),),
872 )
873 return perl_config_data, env_mod
874 return perl_config_data, None
876 def configure_impl(
877 self,
878 context: "BuildContext",
879 manifest: "HighLevelManifest",
880 **kwargs,
881 ) -> None:
882 perl_config_data, cross_env_mod = self._perl_cross_build_env(context)
883 pkg_config_tool = find_source_level_build_tool(
884 context.dpkg_architecture_variables,
885 tool_variable="PKG_CONFIG",
886 resolve=True,
887 )
888 replacements = [("PERL_MM_USE_DEFAULT", "1")]
889 if pkg_config_tool:
890 replacements.append(("PKG_CONFIG", pkg_config_tool))
891 removals = ()
892 else:
893 removals = ("PKG_CONFIG",)
894 configure_env = EnvironmentModification(
895 replacements=tuple(replacements),
896 removals=removals,
897 )
898 if cross_env_mod is not None:
899 configure_env = configure_env.combine(cross_env_mod)
901 configure_cmd = [
902 PERL_CMD,
903 "Build.PL",
904 "--installdirs",
905 "vendor",
906 ]
907 cflags = os.environ.get("CFLAGS", "")
908 cppflags = os.environ.get("CPPFLAGS", "")
909 ldflags = os.environ.get("LDFLAGS", "")
911 if cflags != "" or cppflags != "":
912 configure_cmd.append("--config")
913 combined = f"{cflags} {cppflags}".strip()
914 configure_cmd.append(f"optimize={combined}")
916 if ldflags != "" or cflags != "" or context.is_cross_compiling:
917 configure_cmd.append("--config")
918 combined = f"{perl_config_data.ld} {cflags} {ldflags}".strip()
919 configure_cmd.append(f"ld={combined}")
920 if self.configure_args:
921 substitution = self.substitution
922 attr_path = self.attribute_path["configure_args"]
923 configure_cmd.extend(
924 substitution.substitute(v, attr_path[i].path)
925 for i, v in enumerate(self.configure_args)
926 )
927 run_build_system_command(*configure_cmd, env_mod=configure_env)
929 def build_impl(
930 self,
931 context: "BuildContext",
932 manifest: "HighLevelManifest",
933 **kwargs,
934 ) -> None:
935 _, cross_env_mod = self._perl_cross_build_env(context)
936 run_build_system_command(PERL_CMD, "Build", env_mod=cross_env_mod)
938 def test_impl(
939 self,
940 context: "BuildContext",
941 manifest: "HighLevelManifest",
942 *,
943 should_ignore_test_errors: bool = False,
944 **kwargs,
945 ) -> None:
946 _, cross_env_mod = self._perl_cross_build_env(context)
947 run_build_system_command(
948 PERL_CMD,
949 "Build",
950 "test",
951 "--verbose",
952 "1",
953 env_mod=cross_env_mod,
954 )
956 def install_impl(
957 self,
958 context: "BuildContext",
959 manifest: "HighLevelManifest",
960 dest_dir: str,
961 **kwargs,
962 ) -> None:
963 _, cross_env_mod = self._perl_cross_build_env(context)
964 run_build_system_command(
965 PERL_CMD,
966 "Build",
967 "install",
968 "--destdir",
969 dest_dir,
970 "--create_packlist",
971 "0",
972 env_mod=cross_env_mod,
973 )
975 def clean_impl(
976 self,
977 context: "BuildContext",
978 manifest: "HighLevelManifest",
979 clean_helper: "CleanHelper",
980 **kwargs,
981 ) -> None:
982 _, cross_env_mod = self._perl_cross_build_env(context)
983 if os.path.lexists("Build"):
984 run_build_system_command(
985 PERL_CMD,
986 "Build",
987 "realclean",
988 "--allow_mb_mismatch",
989 "1",
990 env_mod=cross_env_mod,
991 )
994class PerlMakeMakerBuildSystemRule(StepBasedBuildSystemRule):
996 __slots__ = ("configure_args", "_make_support")
998 def __init__(
999 self,
1000 attributes: "ParsedPerlBuildBuildRuleDefinition",
1001 attribute_path: AttributePath,
1002 parser_context: Union[ParserContextData, "HighLevelManifest"],
1003 ) -> None:
1004 super().__init__(attributes, attribute_path, parser_context)
1005 self.configure_args = attributes.get("configure_args", [])
1006 self._make_support = MakefileSupport.from_build_system(self)
1008 @classmethod
1009 def auto_detect_build_system(
1010 cls,
1011 source_root: VirtualPath,
1012 *args,
1013 **kwargs,
1014 ) -> bool:
1015 return "Makefile.PL" in source_root
1017 @classmethod
1018 def characteristics(cls) -> BuildSystemCharacteristics:
1019 return BuildSystemCharacteristics(
1020 out_of_source_builds="not-supported",
1021 )
1023 def configure_impl(
1024 self,
1025 context: "BuildContext",
1026 manifest: "HighLevelManifest",
1027 **kwargs,
1028 ) -> None:
1029 pkg_config_tool = find_source_level_build_tool(
1030 context.dpkg_architecture_variables,
1031 tool_variable="PKG_CONFIG",
1032 resolve=True,
1033 )
1034 replacements = [
1035 ("PERL_MM_USE_DEFAULT", "1"),
1036 ("PERL_AUTOINSTALL", "--skipdeps"),
1037 ]
1038 if pkg_config_tool:
1039 replacements.append(("PKG_CONFIG", pkg_config_tool))
1040 removals = ()
1041 else:
1042 removals = ("PKG_CONFIG",)
1043 configure_env = EnvironmentModification(
1044 replacements=tuple(replacements),
1045 removals=removals,
1046 )
1047 perl_args = []
1048 mm_args = ["INSTALLDIRS=vendor"]
1049 if "CFLAGS" in os.environ:
1050 mm_args.append(
1051 f"OPTIMIZE={os.environ['CFLAGS']} {os.environ['CPPFLAGS']}".rstrip()
1052 )
1054 perl_config_data = resolve_perl_config(
1055 context.dpkg_architecture_variables,
1056 None,
1057 )
1059 if "LDFLAGS" in os.environ:
1060 mm_args.append(
1061 f"LD={perl_config_data.ld} {os.environ['CFLAGS']} {os.environ['LDFLAGS']}"
1062 )
1064 if context.is_cross_compiling:
1065 perl5lib_dir = perl_config_data.cross_inc_dir
1066 if perl5lib_dir is not None:
1067 perl_args.append(f"-I{perl5lib_dir}")
1069 if self.configure_args:
1070 substitution = self.substitution
1071 attr_path = self.attribute_path["configure_args"]
1072 mm_args.extend(
1073 substitution.substitute(v, attr_path[i].path)
1074 for i, v in enumerate(self.configure_args)
1075 )
1076 run_build_system_command(
1077 PERL_CMD,
1078 *perl_args,
1079 "Makefile.PL",
1080 *mm_args,
1081 env_mod=configure_env,
1082 )
1084 def build_impl(
1085 self,
1086 context: "BuildContext",
1087 manifest: "HighLevelManifest",
1088 **kwargs,
1089 ) -> None:
1090 self._make_support.run_make(context)
1092 def test_impl(
1093 self,
1094 context: "BuildContext",
1095 manifest: "HighLevelManifest",
1096 *,
1097 should_ignore_test_errors: bool = False,
1098 **kwargs,
1099 ) -> None:
1100 self._make_support.run_first_existing_target_if_any(
1101 context,
1102 ["check", "test"],
1103 "TEST_VERBOSE=1",
1104 )
1106 def install_impl(
1107 self,
1108 context: "BuildContext",
1109 manifest: "HighLevelManifest",
1110 dest_dir: str,
1111 **kwargs,
1112 ) -> None:
1113 is_mm_makefile = False
1114 with open("Makefile", "rb") as fd:
1115 for line in fd:
1116 if b"generated automatically by MakeMaker" in line:
1117 is_mm_makefile = True
1118 break
1120 install_args = [f"DESTDIR={dest_dir}"]
1122 # Special case for Makefile.PL that uses
1123 # Module::Build::Compat. PREFIX should not be passed
1124 # for those; it already installs into /usr by default.
1125 if is_mm_makefile:
1126 install_args.append("PREFIX=/usr")
1128 self._make_support.run_first_existing_target_if_any(
1129 context,
1130 ["install"],
1131 *install_args,
1132 )
1134 def clean_impl(
1135 self,
1136 context: "BuildContext",
1137 manifest: "HighLevelManifest",
1138 clean_helper: "CleanHelper",
1139 **kwargs,
1140 ) -> None:
1141 self._make_support.run_first_existing_target_if_any(
1142 context,
1143 ["distclean", "realclean", "clean"],
1144 )
1147class DebhelperBuildSystemRule(StepBasedBuildSystemRule):
1149 __slots__ = ("configure_args", "dh_build_system")
1151 def __init__(
1152 self,
1153 parsed_data: "ParsedDebhelperBuildRuleDefinition",
1154 attribute_path: AttributePath,
1155 parser_context: Union[ParserContextData, "HighLevelManifest"],
1156 ) -> None:
1157 super().__init__(parsed_data, attribute_path, parser_context)
1158 self.configure_args = parsed_data.get("configure_args", [])
1159 self.dh_build_system = parsed_data.get("dh_build_system")
1161 @classmethod
1162 def auto_detect_build_system(
1163 cls,
1164 source_root: VirtualPath,
1165 *args,
1166 **kwargs,
1167 ) -> bool:
1168 try:
1169 v = subprocess.check_output(
1170 ["dh_assistant", "which-build-system"],
1171 # Packages without `debhelper-compat` will trigger an error, which will just be noise
1172 stderr=subprocess.DEVNULL,
1173 cwd=source_root.fs_path,
1174 )
1175 except subprocess.CalledProcessError:
1176 return False
1177 d = json.loads(v)
1178 build_system = d.get("build-system")
1179 return build_system is not None and build_system != "none"
1181 @classmethod
1182 def characteristics(cls) -> BuildSystemCharacteristics:
1183 return BuildSystemCharacteristics(
1184 out_of_source_builds="supported-but-not-default",
1185 )
1187 def before_first_impl_step(
1188 self, *, stage: Literal["build", "clean"], **kwargs
1189 ) -> None:
1190 dh_build_system = self.dh_build_system
1191 if dh_build_system is None:
1192 return
1193 try:
1194 subprocess.check_call(
1195 ["dh_assistant", "which-build-system", f"-S{dh_build_system}"]
1196 )
1197 except FileNotFoundError:
1198 _error(
1199 "The debhelper build system assumes `dh_assistant` is available (`debhelper (>= 13.5~)`)"
1200 )
1201 except subprocess.SubprocessError:
1202 raise ManifestInvalidUserDataException(
1203 f'The debhelper build system "{dh_build_system}" does not seem to'
1204 f" be available according to"
1205 f" `dh_assistant which-build-system -S{dh_build_system}`"
1206 ) from None
1208 def _default_options(self) -> list[str]:
1209 default_options = []
1210 if self.dh_build_system is not None:
1211 default_options.append(f"-S{self.dh_build_system}")
1212 if self.build_directory is not None:
1213 default_options.append(f"-B{self.build_directory}")
1215 return default_options
1217 def configure_impl(
1218 self,
1219 context: "BuildContext",
1220 manifest: "HighLevelManifest",
1221 **kwargs,
1222 ) -> None:
1223 if (
1224 os.path.lexists("configure.ac") or os.path.lexists("configure.in")
1225 ) and not os.path.lexists("debian/autoreconf.before"):
1226 run_build_system_command("dh_update_autotools_config")
1227 run_build_system_command("dh_autoreconf")
1229 default_options = self._default_options()
1230 configure_args = default_options.copy()
1231 if self.configure_args:
1232 configure_args.append("--")
1233 substitution = self.substitution
1234 attr_path = self.attribute_path["configure_args"]
1235 configure_args.extend(
1236 substitution.substitute(v, attr_path[i].path)
1237 for i, v in enumerate(self.configure_args)
1238 )
1239 run_build_system_command("dh_auto_configure", *configure_args)
1241 def build_impl(
1242 self,
1243 context: "BuildContext",
1244 manifest: "HighLevelManifest",
1245 **kwargs,
1246 ) -> None:
1247 default_options = self._default_options()
1248 run_build_system_command("dh_auto_build", *default_options)
1250 def test_impl(
1251 self,
1252 context: "BuildContext",
1253 manifest: "HighLevelManifest",
1254 *,
1255 should_ignore_test_errors: bool = False,
1256 **kwargs,
1257 ) -> None:
1258 default_options = self._default_options()
1259 run_build_system_command("dh_auto_test", *default_options)
1261 def install_impl(
1262 self,
1263 context: "BuildContext",
1264 manifest: "HighLevelManifest",
1265 dest_dir: str,
1266 **kwargs,
1267 ) -> None:
1268 default_options = self._default_options()
1269 run_build_system_command(
1270 "dh_auto_install",
1271 *default_options,
1272 f"--destdir={dest_dir}",
1273 )
1275 def clean_impl(
1276 self,
1277 context: "BuildContext",
1278 manifest: "HighLevelManifest",
1279 clean_helper: "CleanHelper",
1280 **kwargs,
1281 ) -> None:
1282 default_options = self._default_options()
1283 run_build_system_command("dh_auto_clean", *default_options)
1284 # The "global" clean logic takes care of `dh_autoreconf_clean` and `dh_clean`
1287class AutoconfBuildSystemRule(StepBasedBuildSystemRule):
1289 __slots__ = ("configure_args", "_make_support")
1291 def __init__(
1292 self,
1293 parsed_data: "ParsedAutoconfBuildRuleDefinition",
1294 attribute_path: AttributePath,
1295 parser_context: Union[ParserContextData, "HighLevelManifest"],
1296 ) -> None:
1297 super().__init__(parsed_data, attribute_path, parser_context)
1298 self.configure_args = list(parsed_data.get("configure_args", []))
1299 self._make_support = MakefileSupport.from_build_system(self)
1301 @classmethod
1302 def characteristics(cls) -> BuildSystemCharacteristics:
1303 return BuildSystemCharacteristics(
1304 out_of_source_builds="supported-and-default",
1305 )
1307 @classmethod
1308 def auto_detect_build_system(
1309 cls,
1310 source_root: VirtualPath,
1311 *args,
1312 **kwargs,
1313 ) -> bool:
1314 if "configure.ac" in source_root:
1315 return True
1316 configure_in = source_root.get("configure.in")
1317 if configure_in is not None and configure_in.is_file:
1318 with configure_in.open(byte_io=True, buffering=4096) as fd:
1319 for no, line in enumerate(fd):
1320 if no > 100: 1320 ↛ 1321line 1320 didn't jump to line 1321 because the condition on line 1320 was never true
1321 break
1322 if b"AC_INIT" in line or b"AC_PREREQ" in line:
1323 return True
1324 configure = source_root.get("configure")
1325 if configure is None or not configure.is_executable or not configure.is_file:
1326 return False
1327 with configure.open(byte_io=True, buffering=4096) as fd:
1328 for no, line in enumerate(fd):
1329 if no > 10: 1329 ↛ 1330line 1329 didn't jump to line 1330 because the condition on line 1329 was never true
1330 break
1331 if b"GNU Autoconf" in line:
1332 return True
1333 return False
1335 def configure_impl(
1336 self,
1337 context: "BuildContext",
1338 manifest: "HighLevelManifest",
1339 **kwargs,
1340 ) -> None:
1341 if (
1342 os.path.lexists("configure.ac") or os.path.lexists("configure.in")
1343 ) and not os.path.lexists("debian/autoreconf.before"):
1344 run_build_system_command("dh_update_autotools_config")
1345 run_build_system_command("dh_autoreconf")
1347 dpkg_architecture_variables = context.dpkg_architecture_variables
1348 multi_arch = dpkg_architecture_variables.current_host_multiarch
1349 silent_rules = (
1350 "--enable-silent-rules"
1351 if context.is_terse_build
1352 else "--disable-silent-rules"
1353 )
1355 configure_args = [
1356 f"--build={dpkg_architecture_variables['DEB_BUILD_GNU_TYPE']}",
1357 "--prefix=/usr",
1358 "--includedir=${prefix}/include",
1359 "--mandir=${prefix}/share/man",
1360 "--infodir=${prefix}/share/info",
1361 "--sysconfdir=/etc",
1362 "--localstatedir=/var",
1363 "--disable-option-checking",
1364 silent_rules,
1365 f"--libdir=${ prefix} /lib/{multi_arch}",
1366 "--runstatedir=/run",
1367 "--disable-maintainer-mode",
1368 "--disable-dependency-tracking",
1369 ]
1370 if dpkg_architecture_variables.is_cross_compiling:
1371 configure_args.append(
1372 f"--host={dpkg_architecture_variables['DEB_HOST_GNU_TYPE']}"
1373 )
1374 if self.configure_args:
1375 substitution = self.substitution
1376 attr_path = self.attribute_path["configure_args"]
1377 configure_args.extend(
1378 substitution.substitute(v, attr_path[i].path)
1379 for i, v in enumerate(self.configure_args)
1380 )
1381 self.ensure_build_dir_exists()
1382 configure_script = self.relative_from_builddir_to_source("configure")
1383 for var in ("CC", "CXX"):
1384 tool = find_source_level_build_tool(
1385 context.dpkg_architecture_variables, tool_variable=var
1386 )
1387 if tool:
1388 os.environ[var] = tool
1389 with self.dump_logs_on_error("config.log"):
1390 run_build_system_command(
1391 configure_script,
1392 *configure_args,
1393 cwd=self.build_directory,
1394 )
1396 def build_impl(
1397 self,
1398 context: "BuildContext",
1399 manifest: "HighLevelManifest",
1400 **kwargs,
1401 ) -> None:
1402 self._make_support.run_make(context)
1404 def test_impl(
1405 self,
1406 context: "BuildContext",
1407 manifest: "HighLevelManifest",
1408 *,
1409 should_ignore_test_errors: bool = False,
1410 **kwargs,
1411 ) -> None:
1412 limit = context.parallelization_limit(support_zero_as_unlimited=True)
1413 testsuite_flags = [f"-j{limit}"] if limit else ["-j"]
1415 if not context.is_terse_build:
1416 testsuite_flags.append("--verbose")
1417 self._make_support.run_first_existing_target_if_any(
1418 context,
1419 # Order is deliberately inverse compared to debhelper (#924052)
1420 ["check", "test"],
1421 f"TESTSUITEFLAGS={' '.join(testsuite_flags)}",
1422 "VERBOSE=1",
1423 )
1425 def install_impl(
1426 self,
1427 context: "BuildContext",
1428 manifest: "HighLevelManifest",
1429 dest_dir: str,
1430 **kwargs,
1431 ) -> None:
1432 enable_parallelization = not os.path.lexists(self.build_dir_path("libtool"))
1433 self._make_support.run_first_existing_target_if_any(
1434 context,
1435 ["install"],
1436 f"DESTDIR={dest_dir}",
1437 "AM_UPDATE_INFO_DIR=no",
1438 enable_parallelization=enable_parallelization,
1439 )
1441 def clean_impl(
1442 self,
1443 context: "BuildContext",
1444 manifest: "HighLevelManifest",
1445 clean_helper: "CleanHelper",
1446 **kwargs,
1447 ) -> None:
1448 if self.out_of_source_build:
1449 return
1450 self._make_support.run_first_existing_target_if_any(
1451 context,
1452 ["distclean", "realclean", "clean"],
1453 )
1454 # The "global" clean logic takes care of `dh_autoreconf_clean` and `dh_clean`
1457class CMakeBuildSystemRule(StepBasedBuildSystemRule):
1459 __slots__ = (
1460 "configure_args",
1461 "target_build_system",
1462 "_make_support",
1463 "_ninja_support",
1464 )
1466 def __init__(
1467 self,
1468 parsed_data: "ParsedCMakeBuildRuleDefinition",
1469 attribute_path: AttributePath,
1470 parser_context: Union[ParserContextData, "HighLevelManifest"],
1471 ) -> None:
1472 super().__init__(parsed_data, attribute_path, parser_context)
1473 self.configure_args = list(parsed_data.get("configure_args", []))
1474 self.target_build_system: Literal["make", "ninja"] = parsed_data.get(
1475 "target_build_system", "make"
1476 )
1477 self._make_support = MakefileSupport.from_build_system(self)
1478 self._ninja_support = NinjaBuildSupport.from_build_system(self)
1480 @classmethod
1481 def characteristics(cls) -> BuildSystemCharacteristics:
1482 return BuildSystemCharacteristics(
1483 out_of_source_builds="required",
1484 )
1486 @classmethod
1487 def auto_detect_build_system(
1488 cls,
1489 source_root: VirtualPath,
1490 *args,
1491 **kwargs,
1492 ) -> bool:
1493 return "CMakeLists.txt" in source_root
1495 @staticmethod
1496 def _default_cmake_env(
1497 build_context: "BuildContext",
1498 ) -> EnvironmentModification:
1499 replacements = {}
1500 if "DEB_PYTHON_INSTALL_LAYOUT" not in os.environ:
1501 replacements["DEB_PYTHON_INSTALL_LAYOUT"] = "deb"
1502 if "PKG_CONFIG" not in os.environ:
1503 pkg_config = build_context.cross_tool("pkg-config")
1504 replacements["PKG_CONFIG"] = f"/usr/bin/{pkg_config}"
1505 return EnvironmentModification(
1506 replacements=tuple((k, v) for k, v in replacements.items())
1507 )
1509 @classmethod
1510 def cmake_generator(cls, target_build_system: Literal["make", "ninja"]) -> str:
1511 cmake_generators = {
1512 "make": "Unix Makefiles",
1513 "ninja": "Ninja",
1514 }
1515 return cmake_generators[target_build_system]
1517 @staticmethod
1518 def _compiler_and_cross_flags(
1519 context: "BuildContext",
1520 cmake_flags: list[str],
1521 ) -> None:
1523 for cmake_name, tool_var, resolve in [
1524 ("CMAKE_C_COMPILER", "CC", False),
1525 ("CMAKE_CXX_COMPILER", "CXX", False),
1526 ("PKG_CONFIG_EXECUTABLE", "PKG_CONFIG", True),
1527 ("PKGCONFIG_EXECUTABLE", "PKG_CONFIG", True),
1528 ("QMAKE_EXECUTABLE", "QMAKE", False),
1529 ]:
1530 tool = find_source_level_build_tool(
1531 context.dpkg_architecture_variables,
1532 tool_variable=tool_var,
1533 resolve=resolve,
1534 )
1535 if tool:
1536 cmake_flags.append(f"-D{cmake_name}={tool}")
1538 if context.is_cross_compiling:
1539 deb_host2cmake_system = {
1540 "linux": "Linux",
1541 "kfreebsd": "kFreeBSD",
1542 "hurd": "GNU",
1543 }
1545 gnu_cpu2system_processor = {
1546 "arm": "armv7l",
1547 "misp64el": "mips64",
1548 "powerpc64le": "ppc64le",
1549 }
1550 dpkg_architecture_variables = context.dpkg_architecture_variables
1552 try:
1553 system_name = deb_host2cmake_system[
1554 dpkg_architecture_variables["DEB_HOST_ARCH_OS"]
1555 ]
1556 except KeyError as e:
1557 name = e.args[0]
1558 _error(
1559 f"Cannot cross-compile via cmake: Missing CMAKE_SYSTEM_NAME for the DEB_HOST_ARCH_OS {name}"
1560 )
1562 gnu_cpu = dpkg_architecture_variables["DEB_HOST_GNU_CPU"]
1563 system_processor = gnu_cpu2system_processor.get(gnu_cpu, gnu_cpu)
1565 cmake_flags.append(f"-DCMAKE_SYSTEM_NAME={system_name}")
1566 cmake_flags.append(f"-DCMAKE_SYSTEM_PROCESSOR={system_processor}")
1568 def configure_impl(
1569 self,
1570 context: "BuildContext",
1571 manifest: "HighLevelManifest",
1572 **kwargs,
1573 ) -> None:
1574 cmake_flags = [
1575 "-DCMAKE_INSTALL_PREFIX=/usr",
1576 "-DCMAKE_BUILD_TYPE=None",
1577 "-DCMAKE_INSTALL_SYSCONFDIR=/etc",
1578 "-DCMAKE_INSTALL_LOCALSTATEDIR=/var",
1579 "-DCMAKE_EXPORT_NO_PACKAGE_REGISTRY=ON",
1580 "-DCMAKE_FIND_USE_PACKAGE_REGISTRY=OFF",
1581 "-DCMAKE_FIND_PACKAGE_NO_PACKAGE_REGISTRY=ON",
1582 "-DFETCHCONTENT_FULLY_DISCONNECTED=ON",
1583 "-DCMAKE_INSTALL_RUNSTATEDIR=/run",
1584 "-DCMAKE_SKIP_INSTALL_ALL_DEPENDENCY=ON",
1585 "-DCMAKE_BUILD_RPATH_USE_ORIGIN=ON",
1586 f"-DCMAKE_INSTALL_LIBDIR=lib/{context.dpkg_architecture_variables['DEB_HOST_MULTIARCH']}",
1587 f"-G{self.cmake_generator(self.target_build_system)}",
1588 ]
1589 if not context.is_terse_build:
1590 cmake_flags.append("-DCMAKE_VERBOSE_MAKEFILE=ON")
1591 if not context.should_run_tests:
1592 cmake_flags.append("-DBUILD_TESTING:BOOL=OFF")
1594 self._compiler_and_cross_flags(context, cmake_flags)
1596 if self.configure_args:
1597 substitution = self.substitution
1598 attr_path = self.attribute_path["configure_args"]
1599 cmake_flags.extend(
1600 substitution.substitute(v, attr_path[i].path)
1601 for i, v in enumerate(self.configure_args)
1602 )
1604 env_mod = self._default_cmake_env(context)
1605 if "CPPFLAGS" in os.environ:
1606 # CMake doesn't respect CPPFLAGS, see #653916.
1607 cppflags = os.environ["CPPFLAGS"]
1608 cflags = f"{os.environ.get('CFLAGS', '')} {cppflags}".lstrip()
1609 cxxflags = f"{os.environ.get('CXXFLAGS', '')} {cppflags}".lstrip()
1610 env_mod = env_mod.combine(
1611 # The debhelper build system never showed this delta, so people might find it annoying.
1612 EnvironmentModification(
1613 replacements=(
1614 ("CFLAGS", cflags),
1615 ("CXXFLAGS", cxxflags),
1616 )
1617 )
1618 )
1619 if "ASMFLAGS" not in os.environ and "ASFLAGS" in os.environ:
1620 env_mod = env_mod.combine(
1621 # The debhelper build system never showed this delta, so people might find it annoying.
1622 EnvironmentModification(
1623 replacements=(("ASMFLAGS", os.environ["ASFLAGS"]),),
1624 )
1625 )
1626 self.ensure_build_dir_exists()
1627 source_dir_from_build_dir = self.relative_from_builddir_to_source()
1629 with self.dump_logs_on_error(
1630 "CMakeCache.txt",
1631 "CMakeFiles/CMakeOutput.log",
1632 "CMakeFiles/CMakeError.log",
1633 ):
1634 run_build_system_command(
1635 "cmake",
1636 *cmake_flags,
1637 source_dir_from_build_dir,
1638 cwd=self.build_directory,
1639 env_mod=env_mod,
1640 )
1642 def build_impl(
1643 self,
1644 context: "BuildContext",
1645 manifest: "HighLevelManifest",
1646 **kwargs,
1647 ) -> None:
1648 if self.target_build_system == "make":
1649 make_flags = []
1650 if not context.is_terse_build:
1651 make_flags.append("VERBOSE=1")
1652 self._make_support.run_make(context, *make_flags)
1653 else:
1654 self._ninja_support.run_ninja_build(context)
1656 def test_impl(
1657 self,
1658 context: "BuildContext",
1659 manifest: "HighLevelManifest",
1660 *,
1661 should_ignore_test_errors: bool = False,
1662 **kwargs,
1663 ) -> None:
1664 env_mod = EnvironmentModification(
1665 replacements=(("CTEST_OUTPUT_ON_FAILURE", "1"),),
1666 )
1667 if self.target_build_system == "make":
1668 # Unlike make, CTest does not have "unlimited parallel" setting (-j implies
1669 # -j1). Therefore, we do not set "allow zero as unlimited" here.
1670 make_flags = [f"ARGS+=-j{context.parallelization_limit()}"]
1671 if not context.is_terse_build:
1672 make_flags.append("ARGS+=--verbose")
1673 self._make_support.run_first_existing_target_if_any(
1674 context,
1675 ["check", "test"],
1676 *make_flags,
1677 env_mod=env_mod,
1678 )
1679 else:
1680 self._ninja_support.run_ninja_test(context, env_mod=env_mod)
1682 limit = context.parallelization_limit(support_zero_as_unlimited=True)
1683 testsuite_flags = [f"-j{limit}"] if limit else ["-j"]
1685 if not context.is_terse_build:
1686 testsuite_flags.append("--verbose")
1687 self._make_support.run_first_existing_target_if_any(
1688 context,
1689 # Order is deliberately inverse compared to debhelper (#924052)
1690 ["check", "test"],
1691 f"TESTSUITEFLAGS={' '.join(testsuite_flags)}",
1692 "VERBOSE=1",
1693 )
1695 def install_impl(
1696 self,
1697 context: "BuildContext",
1698 manifest: "HighLevelManifest",
1699 dest_dir: str,
1700 **kwargs,
1701 ) -> None:
1702 env_mod = EnvironmentModification(
1703 replacements=(
1704 ("LC_ALL", "C.UTF-8"),
1705 ("DESTDIR", dest_dir),
1706 )
1707 ).combine(self._default_cmake_env(context))
1708 run_build_system_command(
1709 "cmake",
1710 "--install",
1711 self.build_directory,
1712 env_mod=env_mod,
1713 )
1715 def clean_impl(
1716 self,
1717 context: "BuildContext",
1718 manifest: "HighLevelManifest",
1719 clean_helper: "CleanHelper",
1720 **kwargs,
1721 ) -> None:
1722 if self.out_of_source_build:
1723 return
1724 if self.target_build_system == "make":
1725 # Keep it here in case we change the `required` "out of source" to "supported-default"
1726 self._make_support.run_first_existing_target_if_any(
1727 context,
1728 ["distclean", "realclean", "clean"],
1729 )
1730 else:
1731 self._ninja_support.run_ninja_clean(context)
1734class MesonBuildSystemRule(StepBasedBuildSystemRule):
1736 __slots__ = (
1737 "configure_args",
1738 "_ninja_support",
1739 )
1741 def __init__(
1742 self,
1743 parsed_data: "ParsedMesonBuildRuleDefinition",
1744 attribute_path: AttributePath,
1745 parser_context: Union[ParserContextData, "HighLevelManifest"],
1746 ) -> None:
1747 super().__init__(parsed_data, attribute_path, parser_context)
1748 self.configure_args = list(parsed_data.get("configure_args", []))
1749 self._ninja_support = NinjaBuildSupport.from_build_system(self)
1751 @classmethod
1752 def characteristics(cls) -> BuildSystemCharacteristics:
1753 return BuildSystemCharacteristics(
1754 out_of_source_builds="required",
1755 )
1757 @classmethod
1758 def auto_detect_build_system(
1759 cls,
1760 source_root: VirtualPath,
1761 *args,
1762 **kwargs,
1763 ) -> bool:
1764 return "meson.build" in source_root
1766 @staticmethod
1767 def _default_meson_env() -> EnvironmentModification:
1768 replacements = {
1769 "LC_ALL": "C.UTF-8",
1770 }
1771 if "DEB_PYTHON_INSTALL_LAYOUT" not in os.environ:
1772 replacements["DEB_PYTHON_INSTALL_LAYOUT"] = "deb"
1773 return EnvironmentModification(
1774 replacements=tuple((k, v) for k, v in replacements.items())
1775 )
1777 @classmethod
1778 def cmake_generator(cls, target_build_system: Literal["make", "ninja"]) -> str:
1779 cmake_generators = {
1780 "make": "Unix Makefiles",
1781 "ninja": "Ninja",
1782 }
1783 return cmake_generators[target_build_system]
1785 @staticmethod
1786 def _cross_flags(
1787 context: "BuildContext",
1788 meson_flags: list[str],
1789 ) -> None:
1790 if not context.is_cross_compiling:
1791 return
1792 # Needs a cross-file http://mesonbuild.com/Cross-compilation.html
1793 cross_files_dir = os.path.abspath(
1794 generated_content_dir(
1795 subdir_key="meson-cross-files",
1796 )
1797 )
1798 host_arch = context.dpkg_architecture_variables.current_host_arch
1799 cross_file = os.path.join(cross_files_dir, f"meson-cross-file-{host_arch}.conf")
1800 if not os.path.isfile(cross_file):
1801 subprocess.check_call(
1802 [
1803 "/usr/share/meson/debcrossgen",
1804 f"--arch={host_arch}",
1805 f"-o{cross_file}",
1806 ],
1807 stdout=subprocess.DEVNULL,
1808 env=collections.ChainMap({"LC_ALL": "C.UTF-8"}, os.environ),
1809 )
1811 meson_flags.append("--cross-file")
1812 meson_flags.append(cross_file)
1814 def configure_impl(
1815 self,
1816 context: "BuildContext",
1817 manifest: "HighLevelManifest",
1818 **kwargs,
1819 ) -> None:
1820 meson_version = Version(
1821 subprocess.check_output(
1822 ["meson", "--version"],
1823 encoding="utf-8",
1824 ).strip()
1825 )
1826 dpkg_architecture_variables = context.dpkg_architecture_variables
1828 meson_flags = [
1829 "--wrap-mode=nodownload",
1830 "--buildtype=plain",
1831 "--prefix=/usr",
1832 "--sysconfdir=/etc",
1833 "--localstatedir=/var",
1834 f"--libdir=lib/{dpkg_architecture_variables.current_host_multiarch}",
1835 "--auto-features=enabled",
1836 ]
1837 if meson_version >= Version("1.2.0"):
1838 # There was a behavior change in Meson 1.2.0: previously
1839 # byte-compilation wasn't supported, but since 1.2.0 it is on by
1840 # default. We can only use this option to turn it off in versions
1841 # where the option exists.
1842 meson_flags.append("-Dpython.bytecompile=-1")
1844 self._cross_flags(context, meson_flags)
1846 if self.configure_args:
1847 substitution = self.substitution
1848 attr_path = self.attribute_path["configure_args"]
1849 meson_flags.extend(
1850 substitution.substitute(v, attr_path[i].path)
1851 for i, v in enumerate(self.configure_args)
1852 )
1854 env_mod = self._default_meson_env()
1856 self.ensure_build_dir_exists()
1857 source_dir_from_build_dir = self.relative_from_builddir_to_source()
1859 with self.dump_logs_on_error("meson-logs/meson-log.txt"):
1860 run_build_system_command(
1861 "meson",
1862 "setup",
1863 source_dir_from_build_dir,
1864 *meson_flags,
1865 cwd=self.build_directory,
1866 env_mod=env_mod,
1867 )
1869 def build_impl(
1870 self,
1871 context: "BuildContext",
1872 manifest: "HighLevelManifest",
1873 **kwargs,
1874 ) -> None:
1875 self._ninja_support.run_ninja_build(context)
1877 def test_impl(
1878 self,
1879 context: "BuildContext",
1880 manifest: "HighLevelManifest",
1881 *,
1882 should_ignore_test_errors: bool = False,
1883 **kwargs,
1884 ) -> None:
1885 env_mod = EnvironmentModification(
1886 replacements=(("MESON_TESTTHREADS", f"{context.parallelization_limit()}"),),
1887 ).combine(self._default_meson_env())
1888 meson_args = []
1889 if not context.is_terse_build:
1890 meson_args.append("--verbose")
1891 with self.dump_logs_on_error("meson-logs/testlog.txt"):
1892 run_build_system_command(
1893 "meson",
1894 "test",
1895 *meson_args,
1896 env_mod=env_mod,
1897 cwd=self.build_directory,
1898 )
1900 def install_impl(
1901 self,
1902 context: "BuildContext",
1903 manifest: "HighLevelManifest",
1904 dest_dir: str,
1905 **kwargs,
1906 ) -> None:
1907 run_build_system_command(
1908 "meson",
1909 "install",
1910 "--no-rebuild",
1911 "--destdir",
1912 dest_dir,
1913 cwd=self.build_directory,
1914 env_mod=self._default_meson_env(),
1915 )
1917 def clean_impl(
1918 self,
1919 context: "BuildContext",
1920 manifest: "HighLevelManifest",
1921 clean_helper: "CleanHelper",
1922 **kwargs,
1923 ) -> None:
1924 # `debputy` will handle all the cleanup for us by virtue of "out of source build"
1925 assert self.out_of_source_build
1928def _add_qmake_flag(options: list[str], envvar: str, *, include_cppflags: bool) -> None:
1929 value = os.environ.get(envvar)
1930 if value is None:
1931 return
1932 if include_cppflags:
1933 cppflags = os.environ.get("CPPFLAGS")
1934 if cppflags:
1935 value = f"{value} {cppflags}"
1937 options.append(f"QMAKE_{envvar}_RELEASE={value}")
1938 options.append(f"QMAKE_{envvar}_DEBUG={value}")
1941class ParsedGenericQmakeBuildRuleDefinition(
1942 OptionalInstallDirectly,
1943 OptionalInSourceBuild,
1944 OptionalBuildDirectory,
1945 OptionalTestRule,
1946):
1947 configure_args: NotRequired[list[str]]
1950class AbstractQmakeBuildSystemRule(StepBasedBuildSystemRule):
1952 __slots__ = ("configure_args", "_make_support")
1954 def __init__(
1955 self,
1956 parsed_data: "ParsedGenericQmakeBuildRuleDefinition",
1957 attribute_path: AttributePath,
1958 parser_context: Union[ParserContextData, "HighLevelManifest"],
1959 ) -> None:
1960 super().__init__(parsed_data, attribute_path, parser_context)
1961 self.configure_args = list(parsed_data.get("configure_args", []))
1962 self._make_support = MakefileSupport.from_build_system(self)
1964 @classmethod
1965 def characteristics(cls) -> BuildSystemCharacteristics:
1966 return BuildSystemCharacteristics(
1967 out_of_source_builds="supported-and-default",
1968 )
1970 @classmethod
1971 def auto_detect_build_system(
1972 cls,
1973 source_root: VirtualPath,
1974 *args,
1975 **kwargs,
1976 ) -> bool:
1977 return any(p.name.endswith(".pro") for p in source_root.iterdir())
1979 @classmethod
1980 def os_mkspec_mapping(cls) -> Mapping[str, str]:
1981 return {
1982 "linux": "linux-g++",
1983 "kfreebsd": "gnukfreebsd-g++",
1984 "hurd": "hurd-g++",
1985 }
1987 def qmake_command_basename(self) -> str:
1988 raise NotImplementedError
1990 def configure_impl(
1991 self,
1992 context: "BuildContext",
1993 manifest: "HighLevelManifest",
1994 **kwargs,
1995 ) -> None:
1997 configure_args = [
1998 "-makefile",
1999 ]
2000 qmake_cmd = find_source_level_build_tool(
2001 context.dpkg_architecture_variables,
2002 command=self.qmake_command_basename(),
2003 required=True,
2004 )
2006 if context.is_cross_compiling:
2007 host_os = context.dpkg_architecture_variables["DEB_HOST_ARCH_OS"]
2008 os2mkspec = self.os_mkspec_mapping()
2009 try:
2010 spec = os2mkspec[host_os]
2011 except KeyError:
2012 _error(
2013 f'Sorry, `debputy` cannot cross build this package for "{host_os}".'
2014 f' Missing a "DEB OS -> qmake -spec <VALUE>" mapping.'
2015 )
2016 configure_args.append("-spec")
2017 configure_args.append(spec)
2019 _add_qmake_flag(configure_args, "CFLAGS", include_cppflags=True)
2020 _add_qmake_flag(configure_args, "CXXFLAGS", include_cppflags=True)
2021 _add_qmake_flag(configure_args, "LDFLAGS", include_cppflags=False)
2023 configure_args.append("QMAKE_STRIP=:")
2024 configure_args.append("PREFIX=/usr")
2026 if self.configure_args:
2027 substitution = self.substitution
2028 attr_path = self.attribute_path["configure_args"]
2029 configure_args.extend(
2030 substitution.substitute(v, attr_path[i].path)
2031 for i, v in enumerate(self.configure_args)
2032 )
2034 self.ensure_build_dir_exists()
2035 if not self.out_of_source_build:
2036 configure_args.append(self.relative_from_builddir_to_source())
2038 with self.dump_logs_on_error("config.log"):
2039 run_build_system_command(
2040 qmake_cmd,
2041 *configure_args,
2042 cwd=self.build_directory,
2043 )
2045 def build_impl(
2046 self,
2047 context: "BuildContext",
2048 manifest: "HighLevelManifest",
2049 **kwargs,
2050 ) -> None:
2051 self._make_support.run_make(context)
2053 def test_impl(
2054 self,
2055 context: "BuildContext",
2056 manifest: "HighLevelManifest",
2057 *,
2058 should_ignore_test_errors: bool = False,
2059 **kwargs,
2060 ) -> None:
2061 limit = context.parallelization_limit(support_zero_as_unlimited=True)
2062 testsuite_flags = [f"-j{limit}"] if limit else ["-j"]
2064 if not context.is_terse_build:
2065 testsuite_flags.append("--verbose")
2066 self._make_support.run_first_existing_target_if_any(
2067 context,
2068 # Order is deliberately inverse compared to debhelper (#924052)
2069 ["check", "test"],
2070 f"TESTSUITEFLAGS={' '.join(testsuite_flags)}",
2071 "VERBOSE=1",
2072 )
2074 def install_impl(
2075 self,
2076 context: "BuildContext",
2077 manifest: "HighLevelManifest",
2078 dest_dir: str,
2079 **kwargs,
2080 ) -> None:
2081 enable_parallelization = not os.path.lexists(self.build_dir_path("libtool"))
2082 self._make_support.run_first_existing_target_if_any(
2083 context,
2084 ["install"],
2085 f"DESTDIR={dest_dir}",
2086 "AM_UPDATE_INFO_DIR=no",
2087 enable_parallelization=enable_parallelization,
2088 )
2090 def clean_impl(
2091 self,
2092 context: "BuildContext",
2093 manifest: "HighLevelManifest",
2094 clean_helper: "CleanHelper",
2095 **kwargs,
2096 ) -> None:
2097 if self.out_of_source_build:
2098 return
2099 self._make_support.run_first_existing_target_if_any(
2100 context,
2101 ["distclean", "realclean", "clean"],
2102 )
2105class QmakeBuildSystemRule(AbstractQmakeBuildSystemRule):
2107 def qmake_command_basename(self) -> str:
2108 return "qmake"
2111class Qmake6BuildSystemRule(AbstractQmakeBuildSystemRule):
2113 def qmake_command_basename(self) -> str:
2114 return "qmake6"
2117@debputy_build_system(
2118 "make",
2119 MakefileBuildSystemRule,
2120 auto_detection_shadows_build_systems="debhelper",
2121 online_reference_documentation=reference_documentation(
2122 title="Make Build System",
2123 description=textwrap.dedent("""\
2124 Run a plain `make` file with nothing else.
2126 This build system will attempt to use `make` to leverage instructions
2127 in a makefile (such as, `Makefile` or `GNUMakefile`).
2129 By default, the makefile build system assumes it should use "in-source"
2130 build semantics. If needed be, an explicit `build-directory` can be
2131 provided if the `Makefile` is not in the source folder but instead in
2132 some other directory.
2133 """),
2134 attributes=[
2135 documented_attr(
2136 "directory",
2137 textwrap.dedent("""\
2138 The directory from which to run make if it is not the source root
2140 This works like using `make -C DIRECTORY ...` (or `cd DIRECTORY && make ...`).
2141 """),
2142 ),
2143 documented_attr(
2144 "build_target",
2145 textwrap.dedent("""\
2146 The target name to use for the "build" step.
2148 If omitted, `make` will be run without any explicit target leaving it to decide
2149 the default.
2150 """),
2151 ),
2152 documented_attr(
2153 "test_target",
2154 textwrap.dedent("""\
2155 The target name to use for the "test" step.
2157 If omitted, `make check` or `make test` will be used if it looks like `make`
2158 will accept one of those targets. Otherwise, the step will be skipped.
2159 """),
2160 ),
2161 documented_attr(
2162 "install_target",
2163 textwrap.dedent("""\
2164 The target name to use for the "install" step.
2166 If omitted, `make install` will be used if it looks like `make` will accept that target.
2167 Otherwise, the step will be skipped.
2168 """),
2169 ),
2170 documented_attr(
2171 "install_target_variables",
2172 textwrap.dedent("""\
2173 Variables (Macros in make lingo) passed when invoking the install target.
2175 This can be used to pass variables to the install target such as `PREFIX=/usr`.
2176 In the `debputy` manifest, they are described as a mapping:
2178 ```
2179 install_target_variables:
2180 PREFIX: "/usr"
2181 ```
2183 The key of the mapping defines the variable name. Remember to match the
2184 case of the variable name that the Makefile expects.
2186 If a variable provided here clashes with a variable that `debputy` provides
2187 by default (such as `DESTDIR` or `INSTALL`), the variable from the manifest
2188 is used instead of the `debputy` default.
2190 The value can be set to `null` to make sure the variable is not passed. That
2191 is, it will suppress any value from `debputy` and let the value from `Makefile`
2192 (if any) take effect. Using the empty string will pass the variable with no value
2193 (such as `make ... FOO=`). This variant will still override the value of the
2194 variable in the Makefile.
2195 """),
2196 ),
2197 *docs_from(
2198 DebputyParsedContentStandardConditional,
2199 OptionalInstallDirectly,
2200 OptionalTestRule,
2201 BuildRuleParsedFormat,
2202 ),
2203 ],
2204 ),
2205)
2206class ParsedMakeBuildRuleDefinition(
2207 OptionalInstallDirectly,
2208 OptionalTestRule,
2209):
2210 directory: NotRequired[FileSystemExactMatchRule]
2211 build_target: NotRequired[str]
2212 test_target: NotRequired[str]
2213 install_target: NotRequired[str]
2214 install_target_variables: NotRequired[dict[str, str | None]]
2217@debputy_build_system(
2218 "autoconf",
2219 AutoconfBuildSystemRule,
2220 auto_detection_shadows_build_systems=["debhelper", "make"],
2221 online_reference_documentation=reference_documentation(
2222 title="Autoconf Build System (`autoconf`)",
2223 description=textwrap.dedent("""\
2224 Run an autoconf-based build system as the upstream build system.
2226 This build rule will attempt to use autoreconf to update the `configure`
2227 script before running the `configure` script if needed. Otherwise, it
2228 follows the classic `./configure && make && make install` pattern.
2230 The build rule uses "out of source" builds by default since it is easier
2231 and more reliable for clean and makes it easier to support multiple
2232 builds (that is, two or more build systems for the same source package).
2233 This is in contract to `debhelper`, which defaults to "in source" builds
2234 for `autoconf`. If you need that behavior, please set
2235 `perform-in-source-build: true`.
2236 """),
2237 attributes=[
2238 documented_attr(
2239 "configure_args",
2240 textwrap.dedent("""\
2241 Arguments to be passed to the `configure` script.
2242 """),
2243 ),
2244 *docs_from(
2245 DebputyParsedContentStandardConditional,
2246 OptionalInstallDirectly,
2247 OptionalInSourceBuild,
2248 OptionalBuildDirectory,
2249 OptionalTestRule,
2250 BuildRuleParsedFormat,
2251 ),
2252 ],
2253 ),
2254)
2255class ParsedAutoconfBuildRuleDefinition(
2256 OptionalInstallDirectly,
2257 OptionalInSourceBuild,
2258 OptionalBuildDirectory,
2259 OptionalTestRule,
2260):
2261 configure_args: NotRequired[list[str]]
2264@debputy_build_system(
2265 "cmake",
2266 CMakeBuildSystemRule,
2267 auto_detection_shadows_build_systems=["debhelper", "make"],
2268 online_reference_documentation=reference_documentation(
2269 title="CMake Build System (`cmake`)",
2270 description=textwrap.dedent("""\
2271 Run an cmake-based build system as the upstream build system.
2273 The build rule uses "out of source" builds.
2274 """),
2275 attributes=[
2276 documented_attr(
2277 "configure_args",
2278 textwrap.dedent("""\
2279 Arguments to be passed to the `cmake` command.
2280 """),
2281 ),
2282 documented_attr(
2283 "target_build_system",
2284 textwrap.dedent("""\
2285 Which build system should `cmake` should delegate the build system to.
2286 In `cmake` terms, this is a Generator (the `-G` option).
2288 Supported options are:
2289 * `make` - GNU Make
2290 * `ninja` - Ninja
2291 """),
2292 ),
2293 *docs_from(
2294 DebputyParsedContentStandardConditional,
2295 OptionalInstallDirectly,
2296 OptionalBuildDirectory,
2297 OptionalTestRule,
2298 BuildRuleParsedFormat,
2299 ),
2300 ],
2301 ),
2302)
2303class ParsedCMakeBuildRuleDefinition(
2304 OptionalInstallDirectly,
2305 OptionalBuildDirectory,
2306 OptionalTestRule,
2307):
2308 configure_args: NotRequired[list[str]]
2309 target_build_system: Literal["make", "ninja"]
2312@debputy_build_system(
2313 "meson",
2314 MesonBuildSystemRule,
2315 auto_detection_shadows_build_systems=["debhelper", "make"],
2316 online_reference_documentation=reference_documentation(
2317 title="Meson Build System (`meson`)",
2318 description=textwrap.dedent("""\
2319 Run an meson-based build system as the upstream build system.
2321 The build rule uses "out of source" builds.
2322 """),
2323 attributes=[
2324 documented_attr(
2325 "configure_args",
2326 textwrap.dedent("""\
2327 Arguments to be passed to the `meson` command.
2328 """),
2329 ),
2330 *docs_from(
2331 DebputyParsedContentStandardConditional,
2332 OptionalInstallDirectly,
2333 OptionalBuildDirectory,
2334 OptionalTestRule,
2335 BuildRuleParsedFormat,
2336 ),
2337 ],
2338 ),
2339)
2340class ParsedMesonBuildRuleDefinition(
2341 OptionalInstallDirectly,
2342 OptionalBuildDirectory,
2343 OptionalTestRule,
2344):
2345 configure_args: NotRequired[list[str]]
2348@debputy_build_system(
2349 "perl-build",
2350 PerlBuildBuildSystemRule,
2351 auto_detection_shadows_build_systems=[
2352 "debhelper",
2353 "make",
2354 "perl-makemaker",
2355 ],
2356 online_reference_documentation=reference_documentation(
2357 title='Perl "Build.PL" Build System (`perl-build`)',
2358 description=textwrap.dedent("""\
2359 Build using the `Build.PL` Build system used by some Perl packages.
2361 This build rule will attempt to use the `Build.PL` script to build the
2362 upstream code.
2363 """),
2364 attributes=[
2365 documented_attr(
2366 "configure_args",
2367 textwrap.dedent("""\
2368 Arguments to be passed to the `Build.PL` script.
2369 """),
2370 ),
2371 *docs_from(
2372 DebputyParsedContentStandardConditional,
2373 OptionalInstallDirectly,
2374 OptionalTestRule,
2375 BuildRuleParsedFormat,
2376 ),
2377 ],
2378 ),
2379)
2380class ParsedPerlBuildBuildRuleDefinition(
2381 OptionalInstallDirectly,
2382 OptionalTestRule,
2383):
2384 configure_args: NotRequired[list[str]]
2387@debputy_build_system(
2388 "debhelper",
2389 DebhelperBuildSystemRule,
2390 online_reference_documentation=reference_documentation(
2391 title="Debhelper Build System (`debhelper`)",
2392 description=textwrap.dedent("""\
2393 Delegate to a debhelper provided build system
2395 This build rule will attempt to use the `dh_auto_*` tools to build the
2396 upstream code. By default, `dh_auto_*` will use auto-detection to determine
2397 which build system they will use. This can be overridden by the
2398 `dh-build-system` attribute.
2399 """),
2400 attributes=[
2401 documented_attr(
2402 "dh_build_system",
2403 textwrap.dedent("""\
2404 Which debhelper build system to use. This attribute is passed to
2405 the `dh_auto_*` commands as the `-S` parameter, so any value valid
2406 for that will be accepted.
2408 Note that many debhelper build systems require extra build
2409 dependencies before they can be used. Please consult the documentation
2410 of the relevant debhelper build system for details.
2411 """),
2412 ),
2413 documented_attr(
2414 "configure_args",
2415 textwrap.dedent("""\
2416 Arguments to be passed to underlying configuration command
2417 (via `dh_auto_configure -- <configure-args`).
2418 """),
2419 ),
2420 *docs_from(
2421 DebputyParsedContentStandardConditional,
2422 OptionalInstallDirectly,
2423 OptionalBuildDirectory,
2424 OptionalTestRule,
2425 BuildRuleParsedFormat,
2426 ),
2427 ],
2428 ),
2429)
2430class ParsedDebhelperBuildRuleDefinition(
2431 OptionalInstallDirectly,
2432 OptionalBuildDirectory,
2433 OptionalTestRule,
2434):
2435 configure_args: NotRequired[list[str]]
2436 dh_build_system: NotRequired[str]
2439@debputy_build_system(
2440 "perl-makemaker",
2441 PerlMakeMakerBuildSystemRule,
2442 auto_detection_shadows_build_systems=[
2443 "debhelper",
2444 "make",
2445 ],
2446 online_reference_documentation=reference_documentation(
2447 title='Perl "MakeMaker" Build System (`perl-makemaker`)',
2448 description=textwrap.dedent("""\
2449 Build using the "MakeMaker" Build system used by some Perl packages.
2451 This build rule will attempt to use the `Makefile.PL` script to build the
2452 upstream code.
2453 """),
2454 attributes=[
2455 documented_attr(
2456 "configure_args",
2457 textwrap.dedent("""\
2458 Arguments to be passed to the `Makefile.PL` script.
2459 """),
2460 ),
2461 *docs_from(
2462 DebputyParsedContentStandardConditional,
2463 OptionalInstallDirectly,
2464 OptionalTestRule,
2465 BuildRuleParsedFormat,
2466 ),
2467 ],
2468 ),
2469)
2470class ParsedPerlMakeMakerBuildRuleDefinition(
2471 OptionalInstallDirectly,
2472 OptionalTestRule,
2473):
2474 configure_args: NotRequired[list[str]]
2477@debputy_build_system(
2478 "qmake",
2479 QmakeBuildSystemRule,
2480 auto_detection_shadows_build_systems=[
2481 "debhelper",
2482 "make",
2483 # Open question, should this shadow "qmake6" and later?
2484 ],
2485 online_reference_documentation=reference_documentation(
2486 title='QT "qmake" Build System (`qmake`)',
2487 description=textwrap.dedent("""\
2488 Build using the "qmake" by QT.
2489 """),
2490 attributes=[
2491 documented_attr(
2492 "configure_args",
2493 textwrap.dedent("""\
2494 Arguments to be passed to the `qmake` command.
2495 """),
2496 ),
2497 *docs_from(
2498 DebputyParsedContentStandardConditional,
2499 OptionalInstallDirectly,
2500 OptionalInSourceBuild,
2501 OptionalBuildDirectory,
2502 OptionalTestRule,
2503 BuildRuleParsedFormat,
2504 ),
2505 ],
2506 ),
2507)
2508class ParsedQmakeBuildRuleDefinition(ParsedGenericQmakeBuildRuleDefinition):
2509 pass
2512@debputy_build_system(
2513 "qmake6",
2514 Qmake6BuildSystemRule,
2515 auto_detection_shadows_build_systems=[
2516 "debhelper",
2517 "make",
2518 ],
2519 online_reference_documentation=reference_documentation(
2520 title='QT "qmake6" Build System (`qmake6`)',
2521 description=textwrap.dedent("""\
2522 Build using the "qmake6" from the `qmake6` package. This is like the `qmake` system
2523 but is specifically for QT6.
2524 """),
2525 attributes=[
2526 documented_attr(
2527 "configure_args",
2528 textwrap.dedent("""\
2529 Arguments to be passed to the `qmake6` command.
2530 """),
2531 ),
2532 *docs_from(
2533 DebputyParsedContentStandardConditional,
2534 OptionalInstallDirectly,
2535 OptionalInSourceBuild,
2536 OptionalBuildDirectory,
2537 OptionalTestRule,
2538 BuildRuleParsedFormat,
2539 ),
2540 ],
2541 ),
2542)
2543class ParsedQmake6BuildRuleDefinition(ParsedGenericQmakeBuildRuleDefinition):
2544 pass
2547def _parse_default_environment(
2548 _name: str,
2549 parsed_data: EnvironmentSourceFormat,
2550 attribute_path: AttributePath,
2551 parser_context: ParserContextData,
2552) -> ManifestProvidedBuildEnvironment:
2553 return ManifestProvidedBuildEnvironment.from_environment_definition(
2554 parsed_data,
2555 attribute_path,
2556 parser_context,
2557 is_default=True,
2558 )
2561def _parse_build_environments(
2562 _name: str,
2563 parsed_data: list[NamedEnvironmentSourceFormat],
2564 attribute_path: AttributePath,
2565 parser_context: ParserContextData,
2566) -> list[ManifestProvidedBuildEnvironment]:
2567 return [
2568 ManifestProvidedBuildEnvironment.from_environment_definition(
2569 value,
2570 attribute_path[idx],
2571 parser_context,
2572 is_default=False,
2573 )
2574 for idx, value in enumerate(parsed_data)
2575 ]
2578def _handle_build_rules(
2579 _name: str,
2580 parsed_data: list[BuildRule],
2581 _attribute_path: AttributePath,
2582 _parser_context: ParserContextData,
2583) -> list[BuildRule]:
2584 return parsed_data