Coverage for src/debputy/plugins/debputy/build_system_rules.py: 34%
741 statements
« prev ^ index » next coverage.py v7.8.2, created at 2026-06-16 19:34 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2026-06-16 19: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 in [
1524 ("CMAKE_C_COMPILER", "CC"),
1525 ("CMAKE_CXX_COMPILER", "CXX"),
1526 ("PKG_CONFIG_EXECUTABLE", "PKG_CONFIG"),
1527 ("PKGCONFIG_EXECUTABLE", "PKG_CONFIG"),
1528 ("QMAKE_EXECUTABLE", "QMAKE"),
1529 ]:
1530 tool = find_source_level_build_tool(
1531 context.dpkg_architecture_variables,
1532 tool_variable=tool_var,
1533 )
1534 if tool:
1535 cmake_flags.append(f"-D{cmake_name}={tool}")
1537 if context.is_cross_compiling:
1538 deb_host2cmake_system = {
1539 "linux": "Linux",
1540 "kfreebsd": "kFreeBSD",
1541 "hurd": "GNU",
1542 }
1544 gnu_cpu2system_processor = {
1545 "arm": "armv7l",
1546 "misp64el": "mips64",
1547 "powerpc64le": "ppc64le",
1548 }
1549 dpkg_architecture_variables = context.dpkg_architecture_variables
1551 try:
1552 system_name = deb_host2cmake_system[
1553 dpkg_architecture_variables["DEB_HOST_ARCH_OS"]
1554 ]
1555 except KeyError as e:
1556 name = e.args[0]
1557 _error(
1558 f"Cannot cross-compile via cmake: Missing CMAKE_SYSTEM_NAME for the DEB_HOST_ARCH_OS {name}"
1559 )
1561 gnu_cpu = dpkg_architecture_variables["DEB_HOST_GNU_CPU"]
1562 system_processor = gnu_cpu2system_processor.get(gnu_cpu, gnu_cpu)
1564 cmake_flags.append(f"-DCMAKE_SYSTEM_NAME={system_name}")
1565 cmake_flags.append(f"-DCMAKE_SYSTEM_PROCESSOR={system_processor}")
1567 def configure_impl(
1568 self,
1569 context: "BuildContext",
1570 manifest: "HighLevelManifest",
1571 **kwargs,
1572 ) -> None:
1573 cmake_flags = [
1574 "-DCMAKE_INSTALL_PREFIX=/usr",
1575 "-DCMAKE_BUILD_TYPE=None",
1576 "-DCMAKE_INSTALL_SYSCONFDIR=/etc",
1577 "-DCMAKE_INSTALL_LOCALSTATEDIR=/var",
1578 "-DCMAKE_EXPORT_NO_PACKAGE_REGISTRY=ON",
1579 "-DCMAKE_FIND_USE_PACKAGE_REGISTRY=OFF",
1580 "-DCMAKE_FIND_PACKAGE_NO_PACKAGE_REGISTRY=ON",
1581 "-DFETCHCONTENT_FULLY_DISCONNECTED=ON",
1582 "-DCMAKE_INSTALL_RUNSTATEDIR=/run",
1583 "-DCMAKE_SKIP_INSTALL_ALL_DEPENDENCY=ON",
1584 "-DCMAKE_BUILD_RPATH_USE_ORIGIN=ON",
1585 f"-DCMAKE_INSTALL_LIBDIR=lib/{context.dpkg_architecture_variables['DEB_HOST_MULTIARCH']}",
1586 f"-G{self.cmake_generator(self.target_build_system)}",
1587 ]
1588 if not context.is_terse_build:
1589 cmake_flags.append("-DCMAKE_VERBOSE_MAKEFILE=ON")
1590 if not context.should_run_tests:
1591 cmake_flags.append("-DBUILD_TESTING:BOOL=OFF")
1593 self._compiler_and_cross_flags(context, cmake_flags)
1595 if self.configure_args:
1596 substitution = self.substitution
1597 attr_path = self.attribute_path["configure_args"]
1598 cmake_flags.extend(
1599 substitution.substitute(v, attr_path[i].path)
1600 for i, v in enumerate(self.configure_args)
1601 )
1603 env_mod = self._default_cmake_env(context)
1604 if "CPPFLAGS" in os.environ:
1605 # CMake doesn't respect CPPFLAGS, see #653916.
1606 cppflags = os.environ["CPPFLAGS"]
1607 cflags = f"{os.environ.get('CFLAGS', '')} {cppflags}".lstrip()
1608 cxxflags = f"{os.environ.get('CXXFLAGS', '')} {cppflags}".lstrip()
1609 env_mod = env_mod.combine(
1610 # The debhelper build system never showed this delta, so people might find it annoying.
1611 EnvironmentModification(
1612 replacements=(
1613 ("CFLAGS", cflags),
1614 ("CXXFLAGS", cxxflags),
1615 )
1616 )
1617 )
1618 if "ASMFLAGS" not in os.environ and "ASFLAGS" in os.environ:
1619 env_mod = env_mod.combine(
1620 # The debhelper build system never showed this delta, so people might find it annoying.
1621 EnvironmentModification(
1622 replacements=(("ASMFLAGS", os.environ["ASFLAGS"]),),
1623 )
1624 )
1625 self.ensure_build_dir_exists()
1626 source_dir_from_build_dir = self.relative_from_builddir_to_source()
1628 with self.dump_logs_on_error(
1629 "CMakeCache.txt",
1630 "CMakeFiles/CMakeOutput.log",
1631 "CMakeFiles/CMakeError.log",
1632 ):
1633 run_build_system_command(
1634 "cmake",
1635 *cmake_flags,
1636 source_dir_from_build_dir,
1637 cwd=self.build_directory,
1638 env_mod=env_mod,
1639 )
1641 def build_impl(
1642 self,
1643 context: "BuildContext",
1644 manifest: "HighLevelManifest",
1645 **kwargs,
1646 ) -> None:
1647 if self.target_build_system == "make":
1648 make_flags = []
1649 if not context.is_terse_build:
1650 make_flags.append("VERBOSE=1")
1651 self._make_support.run_make(context, *make_flags)
1652 else:
1653 self._ninja_support.run_ninja_build(context)
1655 def test_impl(
1656 self,
1657 context: "BuildContext",
1658 manifest: "HighLevelManifest",
1659 *,
1660 should_ignore_test_errors: bool = False,
1661 **kwargs,
1662 ) -> None:
1663 env_mod = EnvironmentModification(
1664 replacements=(("CTEST_OUTPUT_ON_FAILURE", "1"),),
1665 )
1666 if self.target_build_system == "make":
1667 # Unlike make, CTest does not have "unlimited parallel" setting (-j implies
1668 # -j1). Therefore, we do not set "allow zero as unlimited" here.
1669 make_flags = [f"ARGS+=-j{context.parallelization_limit()}"]
1670 if not context.is_terse_build:
1671 make_flags.append("ARGS+=--verbose")
1672 self._make_support.run_first_existing_target_if_any(
1673 context,
1674 ["check", "test"],
1675 *make_flags,
1676 env_mod=env_mod,
1677 )
1678 else:
1679 self._ninja_support.run_ninja_test(context, env_mod=env_mod)
1681 limit = context.parallelization_limit(support_zero_as_unlimited=True)
1682 testsuite_flags = [f"-j{limit}"] if limit else ["-j"]
1684 if not context.is_terse_build:
1685 testsuite_flags.append("--verbose")
1686 self._make_support.run_first_existing_target_if_any(
1687 context,
1688 # Order is deliberately inverse compared to debhelper (#924052)
1689 ["check", "test"],
1690 f"TESTSUITEFLAGS={' '.join(testsuite_flags)}",
1691 "VERBOSE=1",
1692 )
1694 def install_impl(
1695 self,
1696 context: "BuildContext",
1697 manifest: "HighLevelManifest",
1698 dest_dir: str,
1699 **kwargs,
1700 ) -> None:
1701 env_mod = EnvironmentModification(
1702 replacements=(
1703 ("LC_ALL", "C.UTF-8"),
1704 ("DESTDIR", dest_dir),
1705 )
1706 ).combine(self._default_cmake_env(context))
1707 run_build_system_command(
1708 "cmake",
1709 "--install",
1710 self.build_directory,
1711 env_mod=env_mod,
1712 )
1714 def clean_impl(
1715 self,
1716 context: "BuildContext",
1717 manifest: "HighLevelManifest",
1718 clean_helper: "CleanHelper",
1719 **kwargs,
1720 ) -> None:
1721 if self.out_of_source_build:
1722 return
1723 if self.target_build_system == "make":
1724 # Keep it here in case we change the `required` "out of source" to "supported-default"
1725 self._make_support.run_first_existing_target_if_any(
1726 context,
1727 ["distclean", "realclean", "clean"],
1728 )
1729 else:
1730 self._ninja_support.run_ninja_clean(context)
1733class MesonBuildSystemRule(StepBasedBuildSystemRule):
1735 __slots__ = (
1736 "configure_args",
1737 "_ninja_support",
1738 )
1740 def __init__(
1741 self,
1742 parsed_data: "ParsedMesonBuildRuleDefinition",
1743 attribute_path: AttributePath,
1744 parser_context: Union[ParserContextData, "HighLevelManifest"],
1745 ) -> None:
1746 super().__init__(parsed_data, attribute_path, parser_context)
1747 self.configure_args = list(parsed_data.get("configure_args", []))
1748 self._ninja_support = NinjaBuildSupport.from_build_system(self)
1750 @classmethod
1751 def characteristics(cls) -> BuildSystemCharacteristics:
1752 return BuildSystemCharacteristics(
1753 out_of_source_builds="required",
1754 )
1756 @classmethod
1757 def auto_detect_build_system(
1758 cls,
1759 source_root: VirtualPath,
1760 *args,
1761 **kwargs,
1762 ) -> bool:
1763 return "meson.build" in source_root
1765 @staticmethod
1766 def _default_meson_env() -> EnvironmentModification:
1767 replacements = {
1768 "LC_ALL": "C.UTF-8",
1769 }
1770 if "DEB_PYTHON_INSTALL_LAYOUT" not in os.environ:
1771 replacements["DEB_PYTHON_INSTALL_LAYOUT"] = "deb"
1772 return EnvironmentModification(
1773 replacements=tuple((k, v) for k, v in replacements.items())
1774 )
1776 @classmethod
1777 def cmake_generator(cls, target_build_system: Literal["make", "ninja"]) -> str:
1778 cmake_generators = {
1779 "make": "Unix Makefiles",
1780 "ninja": "Ninja",
1781 }
1782 return cmake_generators[target_build_system]
1784 @staticmethod
1785 def _cross_flags(
1786 context: "BuildContext",
1787 meson_flags: list[str],
1788 ) -> None:
1789 if not context.is_cross_compiling:
1790 return
1791 # Needs a cross-file http://mesonbuild.com/Cross-compilation.html
1792 cross_files_dir = os.path.abspath(
1793 generated_content_dir(
1794 subdir_key="meson-cross-files",
1795 )
1796 )
1797 host_arch = context.dpkg_architecture_variables.current_host_arch
1798 cross_file = os.path.join(cross_files_dir, f"meson-cross-file-{host_arch}.conf")
1799 if not os.path.isfile(cross_file):
1800 subprocess.check_call(
1801 [
1802 "/usr/share/meson/debcrossgen",
1803 f"--arch={host_arch}",
1804 f"-o{cross_file}",
1805 ],
1806 stdout=subprocess.DEVNULL,
1807 env=collections.ChainMap({"LC_ALL": "C.UTF-8"}, os.environ),
1808 )
1810 meson_flags.append("--cross-file")
1811 meson_flags.append(cross_file)
1813 def configure_impl(
1814 self,
1815 context: "BuildContext",
1816 manifest: "HighLevelManifest",
1817 **kwargs,
1818 ) -> None:
1819 meson_version = Version(
1820 subprocess.check_output(
1821 ["meson", "--version"],
1822 encoding="utf-8",
1823 ).strip()
1824 )
1825 dpkg_architecture_variables = context.dpkg_architecture_variables
1827 meson_flags = [
1828 "--wrap-mode=nodownload",
1829 "--buildtype=plain",
1830 "--prefix=/usr",
1831 "--sysconfdir=/etc",
1832 "--localstatedir=/var",
1833 f"--libdir=lib/{dpkg_architecture_variables.current_host_multiarch}",
1834 "--auto-features=enabled",
1835 ]
1836 if meson_version >= Version("1.2.0"):
1837 # There was a behavior change in Meson 1.2.0: previously
1838 # byte-compilation wasn't supported, but since 1.2.0 it is on by
1839 # default. We can only use this option to turn it off in versions
1840 # where the option exists.
1841 meson_flags.append("-Dpython.bytecompile=-1")
1843 self._cross_flags(context, meson_flags)
1845 if self.configure_args:
1846 substitution = self.substitution
1847 attr_path = self.attribute_path["configure_args"]
1848 meson_flags.extend(
1849 substitution.substitute(v, attr_path[i].path)
1850 for i, v in enumerate(self.configure_args)
1851 )
1853 env_mod = self._default_meson_env()
1855 self.ensure_build_dir_exists()
1856 source_dir_from_build_dir = self.relative_from_builddir_to_source()
1858 with self.dump_logs_on_error("meson-logs/meson-log.txt"):
1859 run_build_system_command(
1860 "meson",
1861 "setup",
1862 source_dir_from_build_dir,
1863 *meson_flags,
1864 cwd=self.build_directory,
1865 env_mod=env_mod,
1866 )
1868 def build_impl(
1869 self,
1870 context: "BuildContext",
1871 manifest: "HighLevelManifest",
1872 **kwargs,
1873 ) -> None:
1874 self._ninja_support.run_ninja_build(context)
1876 def test_impl(
1877 self,
1878 context: "BuildContext",
1879 manifest: "HighLevelManifest",
1880 *,
1881 should_ignore_test_errors: bool = False,
1882 **kwargs,
1883 ) -> None:
1884 env_mod = EnvironmentModification(
1885 replacements=(("MESON_TESTTHREADS", f"{context.parallelization_limit()}"),),
1886 ).combine(self._default_meson_env())
1887 meson_args = []
1888 if not context.is_terse_build:
1889 meson_args.append("--verbose")
1890 with self.dump_logs_on_error("meson-logs/testlog.txt"):
1891 run_build_system_command(
1892 "meson",
1893 "test",
1894 *meson_args,
1895 env_mod=env_mod,
1896 cwd=self.build_directory,
1897 )
1899 def install_impl(
1900 self,
1901 context: "BuildContext",
1902 manifest: "HighLevelManifest",
1903 dest_dir: str,
1904 **kwargs,
1905 ) -> None:
1906 run_build_system_command(
1907 "meson",
1908 "install",
1909 "--destdir",
1910 dest_dir,
1911 cwd=self.build_directory,
1912 env_mod=self._default_meson_env(),
1913 )
1915 def clean_impl(
1916 self,
1917 context: "BuildContext",
1918 manifest: "HighLevelManifest",
1919 clean_helper: "CleanHelper",
1920 **kwargs,
1921 ) -> None:
1922 # `debputy` will handle all the cleanup for us by virtue of "out of source build"
1923 assert self.out_of_source_build
1926def _add_qmake_flag(options: list[str], envvar: str, *, include_cppflags: bool) -> None:
1927 value = os.environ.get(envvar)
1928 if value is None:
1929 return
1930 if include_cppflags:
1931 cppflags = os.environ.get("CPPFLAGS")
1932 if cppflags:
1933 value = f"{value} {cppflags}"
1935 options.append(f"QMAKE_{envvar}_RELEASE={value}")
1936 options.append(f"QMAKE_{envvar}_DEBUG={value}")
1939class ParsedGenericQmakeBuildRuleDefinition(
1940 OptionalInstallDirectly,
1941 OptionalInSourceBuild,
1942 OptionalBuildDirectory,
1943 OptionalTestRule,
1944):
1945 configure_args: NotRequired[list[str]]
1948class AbstractQmakeBuildSystemRule(StepBasedBuildSystemRule):
1950 __slots__ = ("configure_args", "_make_support")
1952 def __init__(
1953 self,
1954 parsed_data: "ParsedGenericQmakeBuildRuleDefinition",
1955 attribute_path: AttributePath,
1956 parser_context: Union[ParserContextData, "HighLevelManifest"],
1957 ) -> None:
1958 super().__init__(parsed_data, attribute_path, parser_context)
1959 self.configure_args = list(parsed_data.get("configure_args", []))
1960 self._make_support = MakefileSupport.from_build_system(self)
1962 @classmethod
1963 def characteristics(cls) -> BuildSystemCharacteristics:
1964 return BuildSystemCharacteristics(
1965 out_of_source_builds="supported-and-default",
1966 )
1968 @classmethod
1969 def auto_detect_build_system(
1970 cls,
1971 source_root: VirtualPath,
1972 *args,
1973 **kwargs,
1974 ) -> bool:
1975 return any(p.name.endswith(".pro") for p in source_root.iterdir())
1977 @classmethod
1978 def os_mkspec_mapping(cls) -> Mapping[str, str]:
1979 return {
1980 "linux": "linux-g++",
1981 "kfreebsd": "gnukfreebsd-g++",
1982 "hurd": "hurd-g++",
1983 }
1985 def qmake_command_basename(self) -> str:
1986 raise NotImplementedError
1988 def configure_impl(
1989 self,
1990 context: "BuildContext",
1991 manifest: "HighLevelManifest",
1992 **kwargs,
1993 ) -> None:
1995 configure_args = [
1996 "-makefile",
1997 ]
1998 qmake_cmd = find_source_level_build_tool(
1999 context.dpkg_architecture_variables,
2000 command=self.qmake_command_basename(),
2001 required=True,
2002 )
2004 if context.is_cross_compiling:
2005 host_os = context.dpkg_architecture_variables["DEB_HOST_ARCH_OS"]
2006 os2mkspec = self.os_mkspec_mapping()
2007 try:
2008 spec = os2mkspec[host_os]
2009 except KeyError:
2010 _error(
2011 f'Sorry, `debputy` cannot cross build this package for "{host_os}".'
2012 f' Missing a "DEB OS -> qmake -spec <VALUE>" mapping.'
2013 )
2014 configure_args.append("-spec")
2015 configure_args.append(spec)
2017 _add_qmake_flag(configure_args, "CFLAGS", include_cppflags=True)
2018 _add_qmake_flag(configure_args, "CXXFLAGS", include_cppflags=True)
2019 _add_qmake_flag(configure_args, "LDFLAGS", include_cppflags=False)
2021 configure_args.append("QMAKE_STRIP=:")
2022 configure_args.append("PREFIX=/usr")
2024 if self.configure_args:
2025 substitution = self.substitution
2026 attr_path = self.attribute_path["configure_args"]
2027 configure_args.extend(
2028 substitution.substitute(v, attr_path[i].path)
2029 for i, v in enumerate(self.configure_args)
2030 )
2032 self.ensure_build_dir_exists()
2033 if not self.out_of_source_build:
2034 configure_args.append(self.relative_from_builddir_to_source())
2036 with self.dump_logs_on_error("config.log"):
2037 run_build_system_command(
2038 qmake_cmd,
2039 *configure_args,
2040 cwd=self.build_directory,
2041 )
2043 def build_impl(
2044 self,
2045 context: "BuildContext",
2046 manifest: "HighLevelManifest",
2047 **kwargs,
2048 ) -> None:
2049 self._make_support.run_make(context)
2051 def test_impl(
2052 self,
2053 context: "BuildContext",
2054 manifest: "HighLevelManifest",
2055 *,
2056 should_ignore_test_errors: bool = False,
2057 **kwargs,
2058 ) -> None:
2059 limit = context.parallelization_limit(support_zero_as_unlimited=True)
2060 testsuite_flags = [f"-j{limit}"] if limit else ["-j"]
2062 if not context.is_terse_build:
2063 testsuite_flags.append("--verbose")
2064 self._make_support.run_first_existing_target_if_any(
2065 context,
2066 # Order is deliberately inverse compared to debhelper (#924052)
2067 ["check", "test"],
2068 f"TESTSUITEFLAGS={' '.join(testsuite_flags)}",
2069 "VERBOSE=1",
2070 )
2072 def install_impl(
2073 self,
2074 context: "BuildContext",
2075 manifest: "HighLevelManifest",
2076 dest_dir: str,
2077 **kwargs,
2078 ) -> None:
2079 enable_parallelization = not os.path.lexists(self.build_dir_path("libtool"))
2080 self._make_support.run_first_existing_target_if_any(
2081 context,
2082 ["install"],
2083 f"DESTDIR={dest_dir}",
2084 "AM_UPDATE_INFO_DIR=no",
2085 enable_parallelization=enable_parallelization,
2086 )
2088 def clean_impl(
2089 self,
2090 context: "BuildContext",
2091 manifest: "HighLevelManifest",
2092 clean_helper: "CleanHelper",
2093 **kwargs,
2094 ) -> None:
2095 if self.out_of_source_build:
2096 return
2097 self._make_support.run_first_existing_target_if_any(
2098 context,
2099 ["distclean", "realclean", "clean"],
2100 )
2103class QmakeBuildSystemRule(AbstractQmakeBuildSystemRule):
2105 def qmake_command_basename(self) -> str:
2106 return "qmake"
2109class Qmake6BuildSystemRule(AbstractQmakeBuildSystemRule):
2111 def qmake_command_basename(self) -> str:
2112 return "qmake6"
2115@debputy_build_system(
2116 "make",
2117 MakefileBuildSystemRule,
2118 auto_detection_shadows_build_systems="debhelper",
2119 online_reference_documentation=reference_documentation(
2120 title="Make Build System",
2121 description=textwrap.dedent("""\
2122 Run a plain `make` file with nothing else.
2124 This build system will attempt to use `make` to leverage instructions
2125 in a makefile (such as, `Makefile` or `GNUMakefile`).
2127 By default, the makefile build system assumes it should use "in-source"
2128 build semantics. If needed be, an explicit `build-directory` can be
2129 provided if the `Makefile` is not in the source folder but instead in
2130 some other directory.
2131 """),
2132 attributes=[
2133 documented_attr(
2134 "directory",
2135 textwrap.dedent("""\
2136 The directory from which to run make if it is not the source root
2138 This works like using `make -C DIRECTORY ...` (or `cd DIRECTORY && make ...`).
2139 """),
2140 ),
2141 documented_attr(
2142 "build_target",
2143 textwrap.dedent("""\
2144 The target name to use for the "build" step.
2146 If omitted, `make` will be run without any explicit target leaving it to decide
2147 the default.
2148 """),
2149 ),
2150 documented_attr(
2151 "test_target",
2152 textwrap.dedent("""\
2153 The target name to use for the "test" step.
2155 If omitted, `make check` or `make test` will be used if it looks like `make`
2156 will accept one of those targets. Otherwise, the step will be skipped.
2157 """),
2158 ),
2159 documented_attr(
2160 "install_target",
2161 textwrap.dedent("""\
2162 The target name to use for the "install" step.
2164 If omitted, `make install` will be used if it looks like `make` will accept that target.
2165 Otherwise, the step will be skipped.
2166 """),
2167 ),
2168 documented_attr(
2169 "install_target_variables",
2170 textwrap.dedent("""\
2171 Variables (Macros in make lingo) passed when invoking the install target.
2173 This can be used to pass variables to the install target such as `PREFIX=/usr`.
2174 In the `debputy` manifest, they are described as a mapping:
2176 ```
2177 install_target_variables:
2178 PREFIX: "/usr"
2179 ```
2181 The key of the mapping defines the variable name. Remember to match the
2182 case of the variable name that the Makefile expects.
2184 If a variable provided here clashes with a variable that `debputy` provides
2185 by default (such as `DESTDIR` or `INSTALL`), the variable from the manifest
2186 is used instead of the `debputy` default.
2188 The value can be set to `null` to make sure the variable is not passed. That
2189 is, it will suppress any value from `debputy` and let the value from `Makefile`
2190 (if any) take effect. Using the empty string will pass the variable with no value
2191 (such as `make ... FOO=`). This variant will still override the value of the
2192 variable in the Makefile.
2193 """),
2194 ),
2195 *docs_from(
2196 DebputyParsedContentStandardConditional,
2197 OptionalInstallDirectly,
2198 OptionalTestRule,
2199 BuildRuleParsedFormat,
2200 ),
2201 ],
2202 ),
2203)
2204class ParsedMakeBuildRuleDefinition(
2205 OptionalInstallDirectly,
2206 OptionalTestRule,
2207):
2208 directory: NotRequired[FileSystemExactMatchRule]
2209 build_target: NotRequired[str]
2210 test_target: NotRequired[str]
2211 install_target: NotRequired[str]
2212 install_target_variables: NotRequired[dict[str, str | None]]
2215@debputy_build_system(
2216 "autoconf",
2217 AutoconfBuildSystemRule,
2218 auto_detection_shadows_build_systems=["debhelper", "make"],
2219 online_reference_documentation=reference_documentation(
2220 title="Autoconf Build System (`autoconf`)",
2221 description=textwrap.dedent("""\
2222 Run an autoconf-based build system as the upstream build system.
2224 This build rule will attempt to use autoreconf to update the `configure`
2225 script before running the `configure` script if needed. Otherwise, it
2226 follows the classic `./configure && make && make install` pattern.
2228 The build rule uses "out of source" builds by default since it is easier
2229 and more reliable for clean and makes it easier to support multiple
2230 builds (that is, two or more build systems for the same source package).
2231 This is in contract to `debhelper`, which defaults to "in source" builds
2232 for `autoconf`. If you need that behavior, please set
2233 `perform-in-source-build: true`.
2234 """),
2235 attributes=[
2236 documented_attr(
2237 "configure_args",
2238 textwrap.dedent("""\
2239 Arguments to be passed to the `configure` script.
2240 """),
2241 ),
2242 *docs_from(
2243 DebputyParsedContentStandardConditional,
2244 OptionalInstallDirectly,
2245 OptionalInSourceBuild,
2246 OptionalBuildDirectory,
2247 OptionalTestRule,
2248 BuildRuleParsedFormat,
2249 ),
2250 ],
2251 ),
2252)
2253class ParsedAutoconfBuildRuleDefinition(
2254 OptionalInstallDirectly,
2255 OptionalInSourceBuild,
2256 OptionalBuildDirectory,
2257 OptionalTestRule,
2258):
2259 configure_args: NotRequired[list[str]]
2262@debputy_build_system(
2263 "cmake",
2264 CMakeBuildSystemRule,
2265 auto_detection_shadows_build_systems=["debhelper", "make"],
2266 online_reference_documentation=reference_documentation(
2267 title="CMake Build System (`cmake`)",
2268 description=textwrap.dedent("""\
2269 Run an cmake-based build system as the upstream build system.
2271 The build rule uses "out of source" builds.
2272 """),
2273 attributes=[
2274 documented_attr(
2275 "configure_args",
2276 textwrap.dedent("""\
2277 Arguments to be passed to the `cmake` command.
2278 """),
2279 ),
2280 documented_attr(
2281 "target_build_system",
2282 textwrap.dedent("""\
2283 Which build system should `cmake` should delegate the build system to.
2284 In `cmake` terms, this is a Generator (the `-G` option).
2286 Supported options are:
2287 * `make` - GNU Make
2288 * `ninja` - Ninja
2289 """),
2290 ),
2291 *docs_from(
2292 DebputyParsedContentStandardConditional,
2293 OptionalInstallDirectly,
2294 OptionalBuildDirectory,
2295 OptionalTestRule,
2296 BuildRuleParsedFormat,
2297 ),
2298 ],
2299 ),
2300)
2301class ParsedCMakeBuildRuleDefinition(
2302 OptionalInstallDirectly,
2303 OptionalBuildDirectory,
2304 OptionalTestRule,
2305):
2306 configure_args: NotRequired[list[str]]
2307 target_build_system: Literal["make", "ninja"]
2310@debputy_build_system(
2311 "meson",
2312 MesonBuildSystemRule,
2313 auto_detection_shadows_build_systems=["debhelper", "make"],
2314 online_reference_documentation=reference_documentation(
2315 title="Meson Build System (`meson`)",
2316 description=textwrap.dedent("""\
2317 Run an meson-based build system as the upstream build system.
2319 The build rule uses "out of source" builds.
2320 """),
2321 attributes=[
2322 documented_attr(
2323 "configure_args",
2324 textwrap.dedent("""\
2325 Arguments to be passed to the `meson` command.
2326 """),
2327 ),
2328 *docs_from(
2329 DebputyParsedContentStandardConditional,
2330 OptionalInstallDirectly,
2331 OptionalBuildDirectory,
2332 OptionalTestRule,
2333 BuildRuleParsedFormat,
2334 ),
2335 ],
2336 ),
2337)
2338class ParsedMesonBuildRuleDefinition(
2339 OptionalInstallDirectly,
2340 OptionalBuildDirectory,
2341 OptionalTestRule,
2342):
2343 configure_args: NotRequired[list[str]]
2346@debputy_build_system(
2347 "perl-build",
2348 PerlBuildBuildSystemRule,
2349 auto_detection_shadows_build_systems=[
2350 "debhelper",
2351 "make",
2352 "perl-makemaker",
2353 ],
2354 online_reference_documentation=reference_documentation(
2355 title='Perl "Build.PL" Build System (`perl-build`)',
2356 description=textwrap.dedent("""\
2357 Build using the `Build.PL` Build system used by some Perl packages.
2359 This build rule will attempt to use the `Build.PL` script to build the
2360 upstream code.
2361 """),
2362 attributes=[
2363 documented_attr(
2364 "configure_args",
2365 textwrap.dedent("""\
2366 Arguments to be passed to the `Build.PL` script.
2367 """),
2368 ),
2369 *docs_from(
2370 DebputyParsedContentStandardConditional,
2371 OptionalInstallDirectly,
2372 OptionalTestRule,
2373 BuildRuleParsedFormat,
2374 ),
2375 ],
2376 ),
2377)
2378class ParsedPerlBuildBuildRuleDefinition(
2379 OptionalInstallDirectly,
2380 OptionalTestRule,
2381):
2382 configure_args: NotRequired[list[str]]
2385@debputy_build_system(
2386 "debhelper",
2387 DebhelperBuildSystemRule,
2388 online_reference_documentation=reference_documentation(
2389 title="Debhelper Build System (`debhelper`)",
2390 description=textwrap.dedent("""\
2391 Delegate to a debhelper provided build system
2393 This build rule will attempt to use the `dh_auto_*` tools to build the
2394 upstream code. By default, `dh_auto_*` will use auto-detection to determine
2395 which build system they will use. This can be overridden by the
2396 `dh-build-system` attribute.
2397 """),
2398 attributes=[
2399 documented_attr(
2400 "dh_build_system",
2401 textwrap.dedent("""\
2402 Which debhelper build system to use. This attribute is passed to
2403 the `dh_auto_*` commands as the `-S` parameter, so any value valid
2404 for that will be accepted.
2406 Note that many debhelper build systems require extra build
2407 dependencies before they can be used. Please consult the documentation
2408 of the relevant debhelper build system for details.
2409 """),
2410 ),
2411 documented_attr(
2412 "configure_args",
2413 textwrap.dedent("""\
2414 Arguments to be passed to underlying configuration command
2415 (via `dh_auto_configure -- <configure-args`).
2416 """),
2417 ),
2418 *docs_from(
2419 DebputyParsedContentStandardConditional,
2420 OptionalInstallDirectly,
2421 OptionalBuildDirectory,
2422 OptionalTestRule,
2423 BuildRuleParsedFormat,
2424 ),
2425 ],
2426 ),
2427)
2428class ParsedDebhelperBuildRuleDefinition(
2429 OptionalInstallDirectly,
2430 OptionalBuildDirectory,
2431 OptionalTestRule,
2432):
2433 configure_args: NotRequired[list[str]]
2434 dh_build_system: NotRequired[str]
2437@debputy_build_system(
2438 "perl-makemaker",
2439 PerlMakeMakerBuildSystemRule,
2440 auto_detection_shadows_build_systems=[
2441 "debhelper",
2442 "make",
2443 ],
2444 online_reference_documentation=reference_documentation(
2445 title='Perl "MakeMaker" Build System (`perl-makemaker`)',
2446 description=textwrap.dedent("""\
2447 Build using the "MakeMaker" Build system used by some Perl packages.
2449 This build rule will attempt to use the `Makefile.PL` script to build the
2450 upstream code.
2451 """),
2452 attributes=[
2453 documented_attr(
2454 "configure_args",
2455 textwrap.dedent("""\
2456 Arguments to be passed to the `Makefile.PL` script.
2457 """),
2458 ),
2459 *docs_from(
2460 DebputyParsedContentStandardConditional,
2461 OptionalInstallDirectly,
2462 OptionalTestRule,
2463 BuildRuleParsedFormat,
2464 ),
2465 ],
2466 ),
2467)
2468class ParsedPerlMakeMakerBuildRuleDefinition(
2469 OptionalInstallDirectly,
2470 OptionalTestRule,
2471):
2472 configure_args: NotRequired[list[str]]
2475@debputy_build_system(
2476 "qmake",
2477 QmakeBuildSystemRule,
2478 auto_detection_shadows_build_systems=[
2479 "debhelper",
2480 "make",
2481 # Open question, should this shadow "qmake6" and later?
2482 ],
2483 online_reference_documentation=reference_documentation(
2484 title='QT "qmake" Build System (`qmake`)',
2485 description=textwrap.dedent("""\
2486 Build using the "qmake" by QT.
2487 """),
2488 attributes=[
2489 documented_attr(
2490 "configure_args",
2491 textwrap.dedent("""\
2492 Arguments to be passed to the `qmake` command.
2493 """),
2494 ),
2495 *docs_from(
2496 DebputyParsedContentStandardConditional,
2497 OptionalInstallDirectly,
2498 OptionalInSourceBuild,
2499 OptionalBuildDirectory,
2500 OptionalTestRule,
2501 BuildRuleParsedFormat,
2502 ),
2503 ],
2504 ),
2505)
2506class ParsedQmakeBuildRuleDefinition(ParsedGenericQmakeBuildRuleDefinition):
2507 pass
2510@debputy_build_system(
2511 "qmake6",
2512 Qmake6BuildSystemRule,
2513 auto_detection_shadows_build_systems=[
2514 "debhelper",
2515 "make",
2516 ],
2517 online_reference_documentation=reference_documentation(
2518 title='QT "qmake6" Build System (`qmake6`)',
2519 description=textwrap.dedent("""\
2520 Build using the "qmake6" from the `qmake6` package. This is like the `qmake` system
2521 but is specifically for QT6.
2522 """),
2523 attributes=[
2524 documented_attr(
2525 "configure_args",
2526 textwrap.dedent("""\
2527 Arguments to be passed to the `qmake6` command.
2528 """),
2529 ),
2530 *docs_from(
2531 DebputyParsedContentStandardConditional,
2532 OptionalInstallDirectly,
2533 OptionalInSourceBuild,
2534 OptionalBuildDirectory,
2535 OptionalTestRule,
2536 BuildRuleParsedFormat,
2537 ),
2538 ],
2539 ),
2540)
2541class ParsedQmake6BuildRuleDefinition(ParsedGenericQmakeBuildRuleDefinition):
2542 pass
2545def _parse_default_environment(
2546 _name: str,
2547 parsed_data: EnvironmentSourceFormat,
2548 attribute_path: AttributePath,
2549 parser_context: ParserContextData,
2550) -> ManifestProvidedBuildEnvironment:
2551 return ManifestProvidedBuildEnvironment.from_environment_definition(
2552 parsed_data,
2553 attribute_path,
2554 parser_context,
2555 is_default=True,
2556 )
2559def _parse_build_environments(
2560 _name: str,
2561 parsed_data: list[NamedEnvironmentSourceFormat],
2562 attribute_path: AttributePath,
2563 parser_context: ParserContextData,
2564) -> list[ManifestProvidedBuildEnvironment]:
2565 return [
2566 ManifestProvidedBuildEnvironment.from_environment_definition(
2567 value,
2568 attribute_path[idx],
2569 parser_context,
2570 is_default=False,
2571 )
2572 for idx, value in enumerate(parsed_data)
2573 ]
2576def _handle_build_rules(
2577 _name: str,
2578 parsed_data: list[BuildRule],
2579 _attribute_path: AttributePath,
2580 _parser_context: ParserContextData,
2581) -> list[BuildRule]:
2582 return parsed_data