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

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 

20 

21from debian.debian_support import Version 

22 

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) 

77 

78if TYPE_CHECKING: 

79 from debputy.build_support.build_context import BuildContext 

80 from debputy.highlevel_manifest import HighLevelManifest 

81 

82 

83PERL_CMD = "perl" 

84 

85TOOL_VARIABLES = { 

86 "CC": "gcc", 

87 "CXX": "g++", 

88 "PKG_CONFIG": "pkg-config", 

89 "QMAKE": "qmake", 

90} 

91 

92 

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: ... 

103 

104 

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: ... 

115 

116 

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 

145 

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 

152 

153 if not cmd: 

154 raise ValueError( 

155 "At least one of 'tool_variable' and 'command' must be passed and non-empty" 

156 ) 

157 

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 ) 

177 

178 

179class Conditional(DebputyParsedContent): 

180 when: Required[ManifestCondition] 

181 

182 

183def register_build_system_rules(api: DebputyPluginInitializerProvider) -> None: 

184 register_build_keywords(api) 

185 register_build_rules(api) 

186 

187 

188def register_build_keywords(api: DebputyPluginInitializerProvider) -> None: 

189 

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. 

202 

203 The environment definitions can be used to tweak the environment variables used by the 

204 build commands. An example: 

205 

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 

214 

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. 

218 

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). 

224 

225 Accordingly, both `override` and `unset` will overrule any computed variables while 

226 `set` will be overruled by any computed variables. 

227 

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 

236 

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. 

244 

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. 

255 

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. 

264 

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. 

287 

288 The environment definition can be used to tweak the environment variables used by the 

289 build commands. An example: 

290 

291 default-build-environment: 

292 set: 

293 ENV_VAR: foo 

294 ANOTHER_ENV_VAR: bar 

295 

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. 

299 

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). 

305 

306 Accordingly, both `override` and `unset` will overrule any computed variables while 

307 `set` will be overruled by any computed variables. 

308 

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. 

317 

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. 

328 

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. 

337 

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. 

360 

361 A simple example is: 

362 

363 ```yaml 

364 builds: 

365 - autoconf: 

366 configure-args: 

367 - "--enable-foo" 

368 - "--without=bar" 

369 ``` 

370 

371 Multiple builds are supported out of box. Here an example of how the build rules 

372 for `libpam-krb5` might look. 

373 

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 ) 

398 

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. 

411 

412 Example: 

413 

414 ```yaml 

415 ...: 

416 # The tests require internet access and cannot be run at build time. 

417 test-rule: skip-tests 

418 ``` 

419 

420 """), 

421 ), 

422 ) 

423 

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. 

439 

440 Note if you want to only run the tests when the conditional evaluates to `true`, 

441 then wrap the conditional with `not:`. 

442 

443 Example: 

444 

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 ``` 

453 

454 """), 

455 ), 

456 ) 

457 

458 

459def register_build_rules(api: DebputyPluginInitializerProvider) -> None: 

460 api.register_build_system(ParsedAutoconfBuildRuleDefinition) 

461 api.register_build_system(ParsedMakeBuildRuleDefinition) 

462 

463 api.register_build_system(ParsedPerlBuildBuildRuleDefinition) 

464 api.register_build_system(ParsedPerlMakeMakerBuildRuleDefinition) 

465 api.register_build_system(ParsedDebhelperBuildRuleDefinition) 

466 

467 api.register_build_system(ParsedCMakeBuildRuleDefinition) 

468 api.register_build_system(ParsedMesonBuildRuleDefinition) 

469 

470 api.register_build_system(ParsedQmakeBuildRuleDefinition) 

471 api.register_build_system(ParsedQmake6BuildRuleDefinition) 

472 

473 

474class EnvironmentSourceFormat(TypedDict): 

475 set: NotRequired[dict[str, str]] 

476 override: NotRequired[dict[str, str]] 

477 unset: NotRequired[list[str]] 

478 

479 

480class NamedEnvironmentSourceFormat(EnvironmentSourceFormat): 

481 name: str 

482 

483 

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} 

497 

498 

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 ) 

522 

523 

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 ) 

545 

546 

547@dataclasses.dataclass(slots=True, frozen=True) 

548class ManifestProvidedBuildEnvironment(BuildEnvironmentDefinition): 

549 

550 name: str 

551 is_default: bool 

552 attribute_path: AttributePath 

553 parser_context: ParserContextData 

554 

555 set_vars: Mapping[str, str] 

556 override_vars: Mapping[str, str] 

557 unset_vars: Sequence[str] 

558 

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 

575 

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"]) 

582 

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 ) 

588 

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 ) 

613 

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 ) 

629 

630 return r 

631 

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 

655 

656 

657_MAKE_DEFAULT_TOOLS = [ 

658 "CC", 

659 "CXX", 

660 "PKG_CONFIG", 

661] 

662 

663 

664class MakefileBuildSystemRule(StepBasedBuildSystemRule): 

665 

666 __slots__ = ("_make_support", "_build_target", "_install_target", "_directory") 

667 

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 ) 

686 

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 

704 

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")) 

713 

714 @classmethod 

715 def characteristics(cls) -> BuildSystemCharacteristics: 

716 return BuildSystemCharacteristics( 

717 out_of_source_builds="not-supported", 

718 ) 

719 

720 def configure_impl( 

721 self, 

722 context: "BuildContext", 

723 manifest: "HighLevelManifest", 

724 **kwargs, 

725 ) -> None: 

726 # No configure step 

727 pass 

728 

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 ) 

752 

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 ) 

766 

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 ) 

789 

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 ) 

812 

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 ) 

824 

825 

826class PerlBuildBuildSystemRule(StepBasedBuildSystemRule): 

827 

828 __slots__ = "configure_args" 

829 

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", []) 

838 

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 

847 

848 @classmethod 

849 def characteristics(cls) -> BuildSystemCharacteristics: 

850 return BuildSystemCharacteristics( 

851 out_of_source_builds="not-supported", 

852 ) 

853 

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 

875 

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) 

900 

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", "") 

910 

911 if cflags != "" or cppflags != "": 

912 configure_cmd.append("--config") 

913 combined = f"{cflags} {cppflags}".strip() 

914 configure_cmd.append(f"optimize={combined}") 

915 

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) 

928 

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) 

937 

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 ) 

955 

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 ) 

974 

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 ) 

992 

993 

994class PerlMakeMakerBuildSystemRule(StepBasedBuildSystemRule): 

995 

996 __slots__ = ("configure_args", "_make_support") 

997 

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) 

1007 

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 

1016 

1017 @classmethod 

1018 def characteristics(cls) -> BuildSystemCharacteristics: 

1019 return BuildSystemCharacteristics( 

1020 out_of_source_builds="not-supported", 

1021 ) 

1022 

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 ) 

1053 

1054 perl_config_data = resolve_perl_config( 

1055 context.dpkg_architecture_variables, 

1056 None, 

1057 ) 

1058 

1059 if "LDFLAGS" in os.environ: 

1060 mm_args.append( 

1061 f"LD={perl_config_data.ld} {os.environ['CFLAGS']} {os.environ['LDFLAGS']}" 

1062 ) 

1063 

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}") 

1068 

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 ) 

1083 

1084 def build_impl( 

1085 self, 

1086 context: "BuildContext", 

1087 manifest: "HighLevelManifest", 

1088 **kwargs, 

1089 ) -> None: 

1090 self._make_support.run_make(context) 

1091 

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 ) 

1105 

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 

1119 

1120 install_args = [f"DESTDIR={dest_dir}"] 

1121 

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") 

1127 

1128 self._make_support.run_first_existing_target_if_any( 

1129 context, 

1130 ["install"], 

1131 *install_args, 

1132 ) 

1133 

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 ) 

1145 

1146 

1147class DebhelperBuildSystemRule(StepBasedBuildSystemRule): 

1148 

1149 __slots__ = ("configure_args", "dh_build_system") 

1150 

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") 

1160 

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" 

1180 

1181 @classmethod 

1182 def characteristics(cls) -> BuildSystemCharacteristics: 

1183 return BuildSystemCharacteristics( 

1184 out_of_source_builds="supported-but-not-default", 

1185 ) 

1186 

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 

1207 

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}") 

1214 

1215 return default_options 

1216 

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") 

1228 

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) 

1240 

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) 

1249 

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) 

1260 

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 ) 

1274 

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` 

1285 

1286 

1287class AutoconfBuildSystemRule(StepBasedBuildSystemRule): 

1288 

1289 __slots__ = ("configure_args", "_make_support") 

1290 

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) 

1300 

1301 @classmethod 

1302 def characteristics(cls) -> BuildSystemCharacteristics: 

1303 return BuildSystemCharacteristics( 

1304 out_of_source_builds="supported-and-default", 

1305 ) 

1306 

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 

1334 

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") 

1346 

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 ) 

1354 

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 ) 

1395 

1396 def build_impl( 

1397 self, 

1398 context: "BuildContext", 

1399 manifest: "HighLevelManifest", 

1400 **kwargs, 

1401 ) -> None: 

1402 self._make_support.run_make(context) 

1403 

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"] 

1414 

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 ) 

1424 

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 ) 

1440 

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` 

1455 

1456 

1457class CMakeBuildSystemRule(StepBasedBuildSystemRule): 

1458 

1459 __slots__ = ( 

1460 "configure_args", 

1461 "target_build_system", 

1462 "_make_support", 

1463 "_ninja_support", 

1464 ) 

1465 

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) 

1479 

1480 @classmethod 

1481 def characteristics(cls) -> BuildSystemCharacteristics: 

1482 return BuildSystemCharacteristics( 

1483 out_of_source_builds="required", 

1484 ) 

1485 

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 

1494 

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 ) 

1508 

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] 

1516 

1517 @staticmethod 

1518 def _compiler_and_cross_flags( 

1519 context: "BuildContext", 

1520 cmake_flags: list[str], 

1521 ) -> None: 

1522 

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}") 

1536 

1537 if context.is_cross_compiling: 

1538 deb_host2cmake_system = { 

1539 "linux": "Linux", 

1540 "kfreebsd": "kFreeBSD", 

1541 "hurd": "GNU", 

1542 } 

1543 

1544 gnu_cpu2system_processor = { 

1545 "arm": "armv7l", 

1546 "misp64el": "mips64", 

1547 "powerpc64le": "ppc64le", 

1548 } 

1549 dpkg_architecture_variables = context.dpkg_architecture_variables 

1550 

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 ) 

1560 

1561 gnu_cpu = dpkg_architecture_variables["DEB_HOST_GNU_CPU"] 

1562 system_processor = gnu_cpu2system_processor.get(gnu_cpu, gnu_cpu) 

1563 

1564 cmake_flags.append(f"-DCMAKE_SYSTEM_NAME={system_name}") 

1565 cmake_flags.append(f"-DCMAKE_SYSTEM_PROCESSOR={system_processor}") 

1566 

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") 

1592 

1593 self._compiler_and_cross_flags(context, cmake_flags) 

1594 

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 ) 

1602 

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() 

1627 

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 ) 

1640 

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) 

1654 

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) 

1680 

1681 limit = context.parallelization_limit(support_zero_as_unlimited=True) 

1682 testsuite_flags = [f"-j{limit}"] if limit else ["-j"] 

1683 

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 ) 

1693 

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 ) 

1713 

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) 

1731 

1732 

1733class MesonBuildSystemRule(StepBasedBuildSystemRule): 

1734 

1735 __slots__ = ( 

1736 "configure_args", 

1737 "_ninja_support", 

1738 ) 

1739 

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) 

1749 

1750 @classmethod 

1751 def characteristics(cls) -> BuildSystemCharacteristics: 

1752 return BuildSystemCharacteristics( 

1753 out_of_source_builds="required", 

1754 ) 

1755 

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 

1764 

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 ) 

1775 

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] 

1783 

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 ) 

1809 

1810 meson_flags.append("--cross-file") 

1811 meson_flags.append(cross_file) 

1812 

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 

1826 

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") 

1842 

1843 self._cross_flags(context, meson_flags) 

1844 

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 ) 

1852 

1853 env_mod = self._default_meson_env() 

1854 

1855 self.ensure_build_dir_exists() 

1856 source_dir_from_build_dir = self.relative_from_builddir_to_source() 

1857 

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 ) 

1867 

1868 def build_impl( 

1869 self, 

1870 context: "BuildContext", 

1871 manifest: "HighLevelManifest", 

1872 **kwargs, 

1873 ) -> None: 

1874 self._ninja_support.run_ninja_build(context) 

1875 

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 ) 

1898 

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 ) 

1914 

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 

1924 

1925 

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}" 

1934 

1935 options.append(f"QMAKE_{envvar}_RELEASE={value}") 

1936 options.append(f"QMAKE_{envvar}_DEBUG={value}") 

1937 

1938 

1939class ParsedGenericQmakeBuildRuleDefinition( 

1940 OptionalInstallDirectly, 

1941 OptionalInSourceBuild, 

1942 OptionalBuildDirectory, 

1943 OptionalTestRule, 

1944): 

1945 configure_args: NotRequired[list[str]] 

1946 

1947 

1948class AbstractQmakeBuildSystemRule(StepBasedBuildSystemRule): 

1949 

1950 __slots__ = ("configure_args", "_make_support") 

1951 

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) 

1961 

1962 @classmethod 

1963 def characteristics(cls) -> BuildSystemCharacteristics: 

1964 return BuildSystemCharacteristics( 

1965 out_of_source_builds="supported-and-default", 

1966 ) 

1967 

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()) 

1976 

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 } 

1984 

1985 def qmake_command_basename(self) -> str: 

1986 raise NotImplementedError 

1987 

1988 def configure_impl( 

1989 self, 

1990 context: "BuildContext", 

1991 manifest: "HighLevelManifest", 

1992 **kwargs, 

1993 ) -> None: 

1994 

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 ) 

2003 

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) 

2016 

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) 

2020 

2021 configure_args.append("QMAKE_STRIP=:") 

2022 configure_args.append("PREFIX=/usr") 

2023 

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 ) 

2031 

2032 self.ensure_build_dir_exists() 

2033 if not self.out_of_source_build: 

2034 configure_args.append(self.relative_from_builddir_to_source()) 

2035 

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 ) 

2042 

2043 def build_impl( 

2044 self, 

2045 context: "BuildContext", 

2046 manifest: "HighLevelManifest", 

2047 **kwargs, 

2048 ) -> None: 

2049 self._make_support.run_make(context) 

2050 

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"] 

2061 

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 ) 

2071 

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 ) 

2087 

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 ) 

2101 

2102 

2103class QmakeBuildSystemRule(AbstractQmakeBuildSystemRule): 

2104 

2105 def qmake_command_basename(self) -> str: 

2106 return "qmake" 

2107 

2108 

2109class Qmake6BuildSystemRule(AbstractQmakeBuildSystemRule): 

2110 

2111 def qmake_command_basename(self) -> str: 

2112 return "qmake6" 

2113 

2114 

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. 

2123 

2124 This build system will attempt to use `make` to leverage instructions 

2125 in a makefile (such as, `Makefile` or `GNUMakefile`). 

2126 

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 

2137 

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. 

2145 

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. 

2154 

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. 

2163 

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. 

2172 

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: 

2175 

2176 ``` 

2177 install_target_variables: 

2178 PREFIX: "/usr" 

2179 ``` 

2180 

2181 The key of the mapping defines the variable name. Remember to match the 

2182 case of the variable name that the Makefile expects. 

2183 

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. 

2187 

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]] 

2213 

2214 

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. 

2223 

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. 

2227 

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]] 

2260 

2261 

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. 

2270 

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). 

2285 

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"] 

2308 

2309 

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. 

2318 

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]] 

2344 

2345 

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. 

2358 

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]] 

2383 

2384 

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 

2392 

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. 

2405 

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] 

2435 

2436 

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. 

2448 

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]] 

2473 

2474 

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 

2508 

2509 

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 

2543 

2544 

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 ) 

2557 

2558 

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 ] 

2574 

2575 

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