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-08 18:34 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2026-07-08 18:34 +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 "--destdir",
1911 dest_dir,
1912 cwd=self.build_directory,
1913 env_mod=self._default_meson_env(),
1914 )
1916 def clean_impl(
1917 self,
1918 context: "BuildContext",
1919 manifest: "HighLevelManifest",
1920 clean_helper: "CleanHelper",
1921 **kwargs,
1922 ) -> None:
1923 # `debputy` will handle all the cleanup for us by virtue of "out of source build"
1924 assert self.out_of_source_build
1927def _add_qmake_flag(options: list[str], envvar: str, *, include_cppflags: bool) -> None:
1928 value = os.environ.get(envvar)
1929 if value is None:
1930 return
1931 if include_cppflags:
1932 cppflags = os.environ.get("CPPFLAGS")
1933 if cppflags:
1934 value = f"{value} {cppflags}"
1936 options.append(f"QMAKE_{envvar}_RELEASE={value}")
1937 options.append(f"QMAKE_{envvar}_DEBUG={value}")
1940class ParsedGenericQmakeBuildRuleDefinition(
1941 OptionalInstallDirectly,
1942 OptionalInSourceBuild,
1943 OptionalBuildDirectory,
1944 OptionalTestRule,
1945):
1946 configure_args: NotRequired[list[str]]
1949class AbstractQmakeBuildSystemRule(StepBasedBuildSystemRule):
1951 __slots__ = ("configure_args", "_make_support")
1953 def __init__(
1954 self,
1955 parsed_data: "ParsedGenericQmakeBuildRuleDefinition",
1956 attribute_path: AttributePath,
1957 parser_context: Union[ParserContextData, "HighLevelManifest"],
1958 ) -> None:
1959 super().__init__(parsed_data, attribute_path, parser_context)
1960 self.configure_args = list(parsed_data.get("configure_args", []))
1961 self._make_support = MakefileSupport.from_build_system(self)
1963 @classmethod
1964 def characteristics(cls) -> BuildSystemCharacteristics:
1965 return BuildSystemCharacteristics(
1966 out_of_source_builds="supported-and-default",
1967 )
1969 @classmethod
1970 def auto_detect_build_system(
1971 cls,
1972 source_root: VirtualPath,
1973 *args,
1974 **kwargs,
1975 ) -> bool:
1976 return any(p.name.endswith(".pro") for p in source_root.iterdir())
1978 @classmethod
1979 def os_mkspec_mapping(cls) -> Mapping[str, str]:
1980 return {
1981 "linux": "linux-g++",
1982 "kfreebsd": "gnukfreebsd-g++",
1983 "hurd": "hurd-g++",
1984 }
1986 def qmake_command_basename(self) -> str:
1987 raise NotImplementedError
1989 def configure_impl(
1990 self,
1991 context: "BuildContext",
1992 manifest: "HighLevelManifest",
1993 **kwargs,
1994 ) -> None:
1996 configure_args = [
1997 "-makefile",
1998 ]
1999 qmake_cmd = find_source_level_build_tool(
2000 context.dpkg_architecture_variables,
2001 command=self.qmake_command_basename(),
2002 required=True,
2003 )
2005 if context.is_cross_compiling:
2006 host_os = context.dpkg_architecture_variables["DEB_HOST_ARCH_OS"]
2007 os2mkspec = self.os_mkspec_mapping()
2008 try:
2009 spec = os2mkspec[host_os]
2010 except KeyError:
2011 _error(
2012 f'Sorry, `debputy` cannot cross build this package for "{host_os}".'
2013 f' Missing a "DEB OS -> qmake -spec <VALUE>" mapping.'
2014 )
2015 configure_args.append("-spec")
2016 configure_args.append(spec)
2018 _add_qmake_flag(configure_args, "CFLAGS", include_cppflags=True)
2019 _add_qmake_flag(configure_args, "CXXFLAGS", include_cppflags=True)
2020 _add_qmake_flag(configure_args, "LDFLAGS", include_cppflags=False)
2022 configure_args.append("QMAKE_STRIP=:")
2023 configure_args.append("PREFIX=/usr")
2025 if self.configure_args:
2026 substitution = self.substitution
2027 attr_path = self.attribute_path["configure_args"]
2028 configure_args.extend(
2029 substitution.substitute(v, attr_path[i].path)
2030 for i, v in enumerate(self.configure_args)
2031 )
2033 self.ensure_build_dir_exists()
2034 if not self.out_of_source_build:
2035 configure_args.append(self.relative_from_builddir_to_source())
2037 with self.dump_logs_on_error("config.log"):
2038 run_build_system_command(
2039 qmake_cmd,
2040 *configure_args,
2041 cwd=self.build_directory,
2042 )
2044 def build_impl(
2045 self,
2046 context: "BuildContext",
2047 manifest: "HighLevelManifest",
2048 **kwargs,
2049 ) -> None:
2050 self._make_support.run_make(context)
2052 def test_impl(
2053 self,
2054 context: "BuildContext",
2055 manifest: "HighLevelManifest",
2056 *,
2057 should_ignore_test_errors: bool = False,
2058 **kwargs,
2059 ) -> None:
2060 limit = context.parallelization_limit(support_zero_as_unlimited=True)
2061 testsuite_flags = [f"-j{limit}"] if limit else ["-j"]
2063 if not context.is_terse_build:
2064 testsuite_flags.append("--verbose")
2065 self._make_support.run_first_existing_target_if_any(
2066 context,
2067 # Order is deliberately inverse compared to debhelper (#924052)
2068 ["check", "test"],
2069 f"TESTSUITEFLAGS={' '.join(testsuite_flags)}",
2070 "VERBOSE=1",
2071 )
2073 def install_impl(
2074 self,
2075 context: "BuildContext",
2076 manifest: "HighLevelManifest",
2077 dest_dir: str,
2078 **kwargs,
2079 ) -> None:
2080 enable_parallelization = not os.path.lexists(self.build_dir_path("libtool"))
2081 self._make_support.run_first_existing_target_if_any(
2082 context,
2083 ["install"],
2084 f"DESTDIR={dest_dir}",
2085 "AM_UPDATE_INFO_DIR=no",
2086 enable_parallelization=enable_parallelization,
2087 )
2089 def clean_impl(
2090 self,
2091 context: "BuildContext",
2092 manifest: "HighLevelManifest",
2093 clean_helper: "CleanHelper",
2094 **kwargs,
2095 ) -> None:
2096 if self.out_of_source_build:
2097 return
2098 self._make_support.run_first_existing_target_if_any(
2099 context,
2100 ["distclean", "realclean", "clean"],
2101 )
2104class QmakeBuildSystemRule(AbstractQmakeBuildSystemRule):
2106 def qmake_command_basename(self) -> str:
2107 return "qmake"
2110class Qmake6BuildSystemRule(AbstractQmakeBuildSystemRule):
2112 def qmake_command_basename(self) -> str:
2113 return "qmake6"
2116@debputy_build_system(
2117 "make",
2118 MakefileBuildSystemRule,
2119 auto_detection_shadows_build_systems="debhelper",
2120 online_reference_documentation=reference_documentation(
2121 title="Make Build System",
2122 description=textwrap.dedent("""\
2123 Run a plain `make` file with nothing else.
2125 This build system will attempt to use `make` to leverage instructions
2126 in a makefile (such as, `Makefile` or `GNUMakefile`).
2128 By default, the makefile build system assumes it should use "in-source"
2129 build semantics. If needed be, an explicit `build-directory` can be
2130 provided if the `Makefile` is not in the source folder but instead in
2131 some other directory.
2132 """),
2133 attributes=[
2134 documented_attr(
2135 "directory",
2136 textwrap.dedent("""\
2137 The directory from which to run make if it is not the source root
2139 This works like using `make -C DIRECTORY ...` (or `cd DIRECTORY && make ...`).
2140 """),
2141 ),
2142 documented_attr(
2143 "build_target",
2144 textwrap.dedent("""\
2145 The target name to use for the "build" step.
2147 If omitted, `make` will be run without any explicit target leaving it to decide
2148 the default.
2149 """),
2150 ),
2151 documented_attr(
2152 "test_target",
2153 textwrap.dedent("""\
2154 The target name to use for the "test" step.
2156 If omitted, `make check` or `make test` will be used if it looks like `make`
2157 will accept one of those targets. Otherwise, the step will be skipped.
2158 """),
2159 ),
2160 documented_attr(
2161 "install_target",
2162 textwrap.dedent("""\
2163 The target name to use for the "install" step.
2165 If omitted, `make install` will be used if it looks like `make` will accept that target.
2166 Otherwise, the step will be skipped.
2167 """),
2168 ),
2169 documented_attr(
2170 "install_target_variables",
2171 textwrap.dedent("""\
2172 Variables (Macros in make lingo) passed when invoking the install target.
2174 This can be used to pass variables to the install target such as `PREFIX=/usr`.
2175 In the `debputy` manifest, they are described as a mapping:
2177 ```
2178 install_target_variables:
2179 PREFIX: "/usr"
2180 ```
2182 The key of the mapping defines the variable name. Remember to match the
2183 case of the variable name that the Makefile expects.
2185 If a variable provided here clashes with a variable that `debputy` provides
2186 by default (such as `DESTDIR` or `INSTALL`), the variable from the manifest
2187 is used instead of the `debputy` default.
2189 The value can be set to `null` to make sure the variable is not passed. That
2190 is, it will suppress any value from `debputy` and let the value from `Makefile`
2191 (if any) take effect. Using the empty string will pass the variable with no value
2192 (such as `make ... FOO=`). This variant will still override the value of the
2193 variable in the Makefile.
2194 """),
2195 ),
2196 *docs_from(
2197 DebputyParsedContentStandardConditional,
2198 OptionalInstallDirectly,
2199 OptionalTestRule,
2200 BuildRuleParsedFormat,
2201 ),
2202 ],
2203 ),
2204)
2205class ParsedMakeBuildRuleDefinition(
2206 OptionalInstallDirectly,
2207 OptionalTestRule,
2208):
2209 directory: NotRequired[FileSystemExactMatchRule]
2210 build_target: NotRequired[str]
2211 test_target: NotRequired[str]
2212 install_target: NotRequired[str]
2213 install_target_variables: NotRequired[dict[str, str | None]]
2216@debputy_build_system(
2217 "autoconf",
2218 AutoconfBuildSystemRule,
2219 auto_detection_shadows_build_systems=["debhelper", "make"],
2220 online_reference_documentation=reference_documentation(
2221 title="Autoconf Build System (`autoconf`)",
2222 description=textwrap.dedent("""\
2223 Run an autoconf-based build system as the upstream build system.
2225 This build rule will attempt to use autoreconf to update the `configure`
2226 script before running the `configure` script if needed. Otherwise, it
2227 follows the classic `./configure && make && make install` pattern.
2229 The build rule uses "out of source" builds by default since it is easier
2230 and more reliable for clean and makes it easier to support multiple
2231 builds (that is, two or more build systems for the same source package).
2232 This is in contract to `debhelper`, which defaults to "in source" builds
2233 for `autoconf`. If you need that behavior, please set
2234 `perform-in-source-build: true`.
2235 """),
2236 attributes=[
2237 documented_attr(
2238 "configure_args",
2239 textwrap.dedent("""\
2240 Arguments to be passed to the `configure` script.
2241 """),
2242 ),
2243 *docs_from(
2244 DebputyParsedContentStandardConditional,
2245 OptionalInstallDirectly,
2246 OptionalInSourceBuild,
2247 OptionalBuildDirectory,
2248 OptionalTestRule,
2249 BuildRuleParsedFormat,
2250 ),
2251 ],
2252 ),
2253)
2254class ParsedAutoconfBuildRuleDefinition(
2255 OptionalInstallDirectly,
2256 OptionalInSourceBuild,
2257 OptionalBuildDirectory,
2258 OptionalTestRule,
2259):
2260 configure_args: NotRequired[list[str]]
2263@debputy_build_system(
2264 "cmake",
2265 CMakeBuildSystemRule,
2266 auto_detection_shadows_build_systems=["debhelper", "make"],
2267 online_reference_documentation=reference_documentation(
2268 title="CMake Build System (`cmake`)",
2269 description=textwrap.dedent("""\
2270 Run an cmake-based build system as the upstream build system.
2272 The build rule uses "out of source" builds.
2273 """),
2274 attributes=[
2275 documented_attr(
2276 "configure_args",
2277 textwrap.dedent("""\
2278 Arguments to be passed to the `cmake` command.
2279 """),
2280 ),
2281 documented_attr(
2282 "target_build_system",
2283 textwrap.dedent("""\
2284 Which build system should `cmake` should delegate the build system to.
2285 In `cmake` terms, this is a Generator (the `-G` option).
2287 Supported options are:
2288 * `make` - GNU Make
2289 * `ninja` - Ninja
2290 """),
2291 ),
2292 *docs_from(
2293 DebputyParsedContentStandardConditional,
2294 OptionalInstallDirectly,
2295 OptionalBuildDirectory,
2296 OptionalTestRule,
2297 BuildRuleParsedFormat,
2298 ),
2299 ],
2300 ),
2301)
2302class ParsedCMakeBuildRuleDefinition(
2303 OptionalInstallDirectly,
2304 OptionalBuildDirectory,
2305 OptionalTestRule,
2306):
2307 configure_args: NotRequired[list[str]]
2308 target_build_system: Literal["make", "ninja"]
2311@debputy_build_system(
2312 "meson",
2313 MesonBuildSystemRule,
2314 auto_detection_shadows_build_systems=["debhelper", "make"],
2315 online_reference_documentation=reference_documentation(
2316 title="Meson Build System (`meson`)",
2317 description=textwrap.dedent("""\
2318 Run an meson-based build system as the upstream build system.
2320 The build rule uses "out of source" builds.
2321 """),
2322 attributes=[
2323 documented_attr(
2324 "configure_args",
2325 textwrap.dedent("""\
2326 Arguments to be passed to the `meson` command.
2327 """),
2328 ),
2329 *docs_from(
2330 DebputyParsedContentStandardConditional,
2331 OptionalInstallDirectly,
2332 OptionalBuildDirectory,
2333 OptionalTestRule,
2334 BuildRuleParsedFormat,
2335 ),
2336 ],
2337 ),
2338)
2339class ParsedMesonBuildRuleDefinition(
2340 OptionalInstallDirectly,
2341 OptionalBuildDirectory,
2342 OptionalTestRule,
2343):
2344 configure_args: NotRequired[list[str]]
2347@debputy_build_system(
2348 "perl-build",
2349 PerlBuildBuildSystemRule,
2350 auto_detection_shadows_build_systems=[
2351 "debhelper",
2352 "make",
2353 "perl-makemaker",
2354 ],
2355 online_reference_documentation=reference_documentation(
2356 title='Perl "Build.PL" Build System (`perl-build`)',
2357 description=textwrap.dedent("""\
2358 Build using the `Build.PL` Build system used by some Perl packages.
2360 This build rule will attempt to use the `Build.PL` script to build the
2361 upstream code.
2362 """),
2363 attributes=[
2364 documented_attr(
2365 "configure_args",
2366 textwrap.dedent("""\
2367 Arguments to be passed to the `Build.PL` script.
2368 """),
2369 ),
2370 *docs_from(
2371 DebputyParsedContentStandardConditional,
2372 OptionalInstallDirectly,
2373 OptionalTestRule,
2374 BuildRuleParsedFormat,
2375 ),
2376 ],
2377 ),
2378)
2379class ParsedPerlBuildBuildRuleDefinition(
2380 OptionalInstallDirectly,
2381 OptionalTestRule,
2382):
2383 configure_args: NotRequired[list[str]]
2386@debputy_build_system(
2387 "debhelper",
2388 DebhelperBuildSystemRule,
2389 online_reference_documentation=reference_documentation(
2390 title="Debhelper Build System (`debhelper`)",
2391 description=textwrap.dedent("""\
2392 Delegate to a debhelper provided build system
2394 This build rule will attempt to use the `dh_auto_*` tools to build the
2395 upstream code. By default, `dh_auto_*` will use auto-detection to determine
2396 which build system they will use. This can be overridden by the
2397 `dh-build-system` attribute.
2398 """),
2399 attributes=[
2400 documented_attr(
2401 "dh_build_system",
2402 textwrap.dedent("""\
2403 Which debhelper build system to use. This attribute is passed to
2404 the `dh_auto_*` commands as the `-S` parameter, so any value valid
2405 for that will be accepted.
2407 Note that many debhelper build systems require extra build
2408 dependencies before they can be used. Please consult the documentation
2409 of the relevant debhelper build system for details.
2410 """),
2411 ),
2412 documented_attr(
2413 "configure_args",
2414 textwrap.dedent("""\
2415 Arguments to be passed to underlying configuration command
2416 (via `dh_auto_configure -- <configure-args`).
2417 """),
2418 ),
2419 *docs_from(
2420 DebputyParsedContentStandardConditional,
2421 OptionalInstallDirectly,
2422 OptionalBuildDirectory,
2423 OptionalTestRule,
2424 BuildRuleParsedFormat,
2425 ),
2426 ],
2427 ),
2428)
2429class ParsedDebhelperBuildRuleDefinition(
2430 OptionalInstallDirectly,
2431 OptionalBuildDirectory,
2432 OptionalTestRule,
2433):
2434 configure_args: NotRequired[list[str]]
2435 dh_build_system: NotRequired[str]
2438@debputy_build_system(
2439 "perl-makemaker",
2440 PerlMakeMakerBuildSystemRule,
2441 auto_detection_shadows_build_systems=[
2442 "debhelper",
2443 "make",
2444 ],
2445 online_reference_documentation=reference_documentation(
2446 title='Perl "MakeMaker" Build System (`perl-makemaker`)',
2447 description=textwrap.dedent("""\
2448 Build using the "MakeMaker" Build system used by some Perl packages.
2450 This build rule will attempt to use the `Makefile.PL` script to build the
2451 upstream code.
2452 """),
2453 attributes=[
2454 documented_attr(
2455 "configure_args",
2456 textwrap.dedent("""\
2457 Arguments to be passed to the `Makefile.PL` script.
2458 """),
2459 ),
2460 *docs_from(
2461 DebputyParsedContentStandardConditional,
2462 OptionalInstallDirectly,
2463 OptionalTestRule,
2464 BuildRuleParsedFormat,
2465 ),
2466 ],
2467 ),
2468)
2469class ParsedPerlMakeMakerBuildRuleDefinition(
2470 OptionalInstallDirectly,
2471 OptionalTestRule,
2472):
2473 configure_args: NotRequired[list[str]]
2476@debputy_build_system(
2477 "qmake",
2478 QmakeBuildSystemRule,
2479 auto_detection_shadows_build_systems=[
2480 "debhelper",
2481 "make",
2482 # Open question, should this shadow "qmake6" and later?
2483 ],
2484 online_reference_documentation=reference_documentation(
2485 title='QT "qmake" Build System (`qmake`)',
2486 description=textwrap.dedent("""\
2487 Build using the "qmake" by QT.
2488 """),
2489 attributes=[
2490 documented_attr(
2491 "configure_args",
2492 textwrap.dedent("""\
2493 Arguments to be passed to the `qmake` command.
2494 """),
2495 ),
2496 *docs_from(
2497 DebputyParsedContentStandardConditional,
2498 OptionalInstallDirectly,
2499 OptionalInSourceBuild,
2500 OptionalBuildDirectory,
2501 OptionalTestRule,
2502 BuildRuleParsedFormat,
2503 ),
2504 ],
2505 ),
2506)
2507class ParsedQmakeBuildRuleDefinition(ParsedGenericQmakeBuildRuleDefinition):
2508 pass
2511@debputy_build_system(
2512 "qmake6",
2513 Qmake6BuildSystemRule,
2514 auto_detection_shadows_build_systems=[
2515 "debhelper",
2516 "make",
2517 ],
2518 online_reference_documentation=reference_documentation(
2519 title='QT "qmake6" Build System (`qmake6`)',
2520 description=textwrap.dedent("""\
2521 Build using the "qmake6" from the `qmake6` package. This is like the `qmake` system
2522 but is specifically for QT6.
2523 """),
2524 attributes=[
2525 documented_attr(
2526 "configure_args",
2527 textwrap.dedent("""\
2528 Arguments to be passed to the `qmake6` command.
2529 """),
2530 ),
2531 *docs_from(
2532 DebputyParsedContentStandardConditional,
2533 OptionalInstallDirectly,
2534 OptionalInSourceBuild,
2535 OptionalBuildDirectory,
2536 OptionalTestRule,
2537 BuildRuleParsedFormat,
2538 ),
2539 ],
2540 ),
2541)
2542class ParsedQmake6BuildRuleDefinition(ParsedGenericQmakeBuildRuleDefinition):
2543 pass
2546def _parse_default_environment(
2547 _name: str,
2548 parsed_data: EnvironmentSourceFormat,
2549 attribute_path: AttributePath,
2550 parser_context: ParserContextData,
2551) -> ManifestProvidedBuildEnvironment:
2552 return ManifestProvidedBuildEnvironment.from_environment_definition(
2553 parsed_data,
2554 attribute_path,
2555 parser_context,
2556 is_default=True,
2557 )
2560def _parse_build_environments(
2561 _name: str,
2562 parsed_data: list[NamedEnvironmentSourceFormat],
2563 attribute_path: AttributePath,
2564 parser_context: ParserContextData,
2565) -> list[ManifestProvidedBuildEnvironment]:
2566 return [
2567 ManifestProvidedBuildEnvironment.from_environment_definition(
2568 value,
2569 attribute_path[idx],
2570 parser_context,
2571 is_default=False,
2572 )
2573 for idx, value in enumerate(parsed_data)
2574 ]
2577def _handle_build_rules(
2578 _name: str,
2579 parsed_data: list[BuildRule],
2580 _attribute_path: AttributePath,
2581 _parser_context: ParserContextData,
2582) -> list[BuildRule]:
2583 return parsed_data