Coverage for src/debputy/plugins/debputy/build_system_rules.py: 33%

715 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-07-08 18:34 +0000

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, resolve in [ 

1524 ("CMAKE_C_COMPILER", "CC", False), 

1525 ("CMAKE_CXX_COMPILER", "CXX", False), 

1526 ("PKG_CONFIG_EXECUTABLE", "PKG_CONFIG", True), 

1527 ("PKGCONFIG_EXECUTABLE", "PKG_CONFIG", True), 

1528 ("QMAKE_EXECUTABLE", "QMAKE", False), 

1529 ]: 

1530 tool = find_source_level_build_tool( 

1531 context.dpkg_architecture_variables, 

1532 tool_variable=tool_var, 

1533 resolve=resolve, 

1534 ) 

1535 if tool: 

1536 cmake_flags.append(f"-D{cmake_name}={tool}") 

1537 

1538 if context.is_cross_compiling: 

1539 deb_host2cmake_system = { 

1540 "linux": "Linux", 

1541 "kfreebsd": "kFreeBSD", 

1542 "hurd": "GNU", 

1543 } 

1544 

1545 gnu_cpu2system_processor = { 

1546 "arm": "armv7l", 

1547 "misp64el": "mips64", 

1548 "powerpc64le": "ppc64le", 

1549 } 

1550 dpkg_architecture_variables = context.dpkg_architecture_variables 

1551 

1552 try: 

1553 system_name = deb_host2cmake_system[ 

1554 dpkg_architecture_variables["DEB_HOST_ARCH_OS"] 

1555 ] 

1556 except KeyError as e: 

1557 name = e.args[0] 

1558 _error( 

1559 f"Cannot cross-compile via cmake: Missing CMAKE_SYSTEM_NAME for the DEB_HOST_ARCH_OS {name}" 

1560 ) 

1561 

1562 gnu_cpu = dpkg_architecture_variables["DEB_HOST_GNU_CPU"] 

1563 system_processor = gnu_cpu2system_processor.get(gnu_cpu, gnu_cpu) 

1564 

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

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

1567 

1568 def configure_impl( 

1569 self, 

1570 context: "BuildContext", 

1571 manifest: "HighLevelManifest", 

1572 **kwargs, 

1573 ) -> None: 

1574 cmake_flags = [ 

1575 "-DCMAKE_INSTALL_PREFIX=/usr", 

1576 "-DCMAKE_BUILD_TYPE=None", 

1577 "-DCMAKE_INSTALL_SYSCONFDIR=/etc", 

1578 "-DCMAKE_INSTALL_LOCALSTATEDIR=/var", 

1579 "-DCMAKE_EXPORT_NO_PACKAGE_REGISTRY=ON", 

1580 "-DCMAKE_FIND_USE_PACKAGE_REGISTRY=OFF", 

1581 "-DCMAKE_FIND_PACKAGE_NO_PACKAGE_REGISTRY=ON", 

1582 "-DFETCHCONTENT_FULLY_DISCONNECTED=ON", 

1583 "-DCMAKE_INSTALL_RUNSTATEDIR=/run", 

1584 "-DCMAKE_SKIP_INSTALL_ALL_DEPENDENCY=ON", 

1585 "-DCMAKE_BUILD_RPATH_USE_ORIGIN=ON", 

1586 f"-DCMAKE_INSTALL_LIBDIR=lib/{context.dpkg_architecture_variables['DEB_HOST_MULTIARCH']}", 

1587 f"-G{self.cmake_generator(self.target_build_system)}", 

1588 ] 

1589 if not context.is_terse_build: 

1590 cmake_flags.append("-DCMAKE_VERBOSE_MAKEFILE=ON") 

1591 if not context.should_run_tests: 

1592 cmake_flags.append("-DBUILD_TESTING:BOOL=OFF") 

1593 

1594 self._compiler_and_cross_flags(context, cmake_flags) 

1595 

1596 if self.configure_args: 

1597 substitution = self.substitution 

1598 attr_path = self.attribute_path["configure_args"] 

1599 cmake_flags.extend( 

1600 substitution.substitute(v, attr_path[i].path) 

1601 for i, v in enumerate(self.configure_args) 

1602 ) 

1603 

1604 env_mod = self._default_cmake_env(context) 

1605 if "CPPFLAGS" in os.environ: 

1606 # CMake doesn't respect CPPFLAGS, see #653916. 

1607 cppflags = os.environ["CPPFLAGS"] 

1608 cflags = f"{os.environ.get('CFLAGS', '')} {cppflags}".lstrip() 

1609 cxxflags = f"{os.environ.get('CXXFLAGS', '')} {cppflags}".lstrip() 

1610 env_mod = env_mod.combine( 

1611 # The debhelper build system never showed this delta, so people might find it annoying. 

1612 EnvironmentModification( 

1613 replacements=( 

1614 ("CFLAGS", cflags), 

1615 ("CXXFLAGS", cxxflags), 

1616 ) 

1617 ) 

1618 ) 

1619 if "ASMFLAGS" not in os.environ and "ASFLAGS" in os.environ: 

1620 env_mod = env_mod.combine( 

1621 # The debhelper build system never showed this delta, so people might find it annoying. 

1622 EnvironmentModification( 

1623 replacements=(("ASMFLAGS", os.environ["ASFLAGS"]),), 

1624 ) 

1625 ) 

1626 self.ensure_build_dir_exists() 

1627 source_dir_from_build_dir = self.relative_from_builddir_to_source() 

1628 

1629 with self.dump_logs_on_error( 

1630 "CMakeCache.txt", 

1631 "CMakeFiles/CMakeOutput.log", 

1632 "CMakeFiles/CMakeError.log", 

1633 ): 

1634 run_build_system_command( 

1635 "cmake", 

1636 *cmake_flags, 

1637 source_dir_from_build_dir, 

1638 cwd=self.build_directory, 

1639 env_mod=env_mod, 

1640 ) 

1641 

1642 def build_impl( 

1643 self, 

1644 context: "BuildContext", 

1645 manifest: "HighLevelManifest", 

1646 **kwargs, 

1647 ) -> None: 

1648 if self.target_build_system == "make": 

1649 make_flags = [] 

1650 if not context.is_terse_build: 

1651 make_flags.append("VERBOSE=1") 

1652 self._make_support.run_make(context, *make_flags) 

1653 else: 

1654 self._ninja_support.run_ninja_build(context) 

1655 

1656 def test_impl( 

1657 self, 

1658 context: "BuildContext", 

1659 manifest: "HighLevelManifest", 

1660 *, 

1661 should_ignore_test_errors: bool = False, 

1662 **kwargs, 

1663 ) -> None: 

1664 env_mod = EnvironmentModification( 

1665 replacements=(("CTEST_OUTPUT_ON_FAILURE", "1"),), 

1666 ) 

1667 if self.target_build_system == "make": 

1668 # Unlike make, CTest does not have "unlimited parallel" setting (-j implies 

1669 # -j1). Therefore, we do not set "allow zero as unlimited" here. 

1670 make_flags = [f"ARGS+=-j{context.parallelization_limit()}"] 

1671 if not context.is_terse_build: 

1672 make_flags.append("ARGS+=--verbose") 

1673 self._make_support.run_first_existing_target_if_any( 

1674 context, 

1675 ["check", "test"], 

1676 *make_flags, 

1677 env_mod=env_mod, 

1678 ) 

1679 else: 

1680 self._ninja_support.run_ninja_test(context, env_mod=env_mod) 

1681 

1682 limit = context.parallelization_limit(support_zero_as_unlimited=True) 

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

1684 

1685 if not context.is_terse_build: 

1686 testsuite_flags.append("--verbose") 

1687 self._make_support.run_first_existing_target_if_any( 

1688 context, 

1689 # Order is deliberately inverse compared to debhelper (#924052) 

1690 ["check", "test"], 

1691 f"TESTSUITEFLAGS={' '.join(testsuite_flags)}", 

1692 "VERBOSE=1", 

1693 ) 

1694 

1695 def install_impl( 

1696 self, 

1697 context: "BuildContext", 

1698 manifest: "HighLevelManifest", 

1699 dest_dir: str, 

1700 **kwargs, 

1701 ) -> None: 

1702 env_mod = EnvironmentModification( 

1703 replacements=( 

1704 ("LC_ALL", "C.UTF-8"), 

1705 ("DESTDIR", dest_dir), 

1706 ) 

1707 ).combine(self._default_cmake_env(context)) 

1708 run_build_system_command( 

1709 "cmake", 

1710 "--install", 

1711 self.build_directory, 

1712 env_mod=env_mod, 

1713 ) 

1714 

1715 def clean_impl( 

1716 self, 

1717 context: "BuildContext", 

1718 manifest: "HighLevelManifest", 

1719 clean_helper: "CleanHelper", 

1720 **kwargs, 

1721 ) -> None: 

1722 if self.out_of_source_build: 

1723 return 

1724 if self.target_build_system == "make": 

1725 # Keep it here in case we change the `required` "out of source" to "supported-default" 

1726 self._make_support.run_first_existing_target_if_any( 

1727 context, 

1728 ["distclean", "realclean", "clean"], 

1729 ) 

1730 else: 

1731 self._ninja_support.run_ninja_clean(context) 

1732 

1733 

1734class MesonBuildSystemRule(StepBasedBuildSystemRule): 

1735 

1736 __slots__ = ( 

1737 "configure_args", 

1738 "_ninja_support", 

1739 ) 

1740 

1741 def __init__( 

1742 self, 

1743 parsed_data: "ParsedMesonBuildRuleDefinition", 

1744 attribute_path: AttributePath, 

1745 parser_context: Union[ParserContextData, "HighLevelManifest"], 

1746 ) -> None: 

1747 super().__init__(parsed_data, attribute_path, parser_context) 

1748 self.configure_args = list(parsed_data.get("configure_args", [])) 

1749 self._ninja_support = NinjaBuildSupport.from_build_system(self) 

1750 

1751 @classmethod 

1752 def characteristics(cls) -> BuildSystemCharacteristics: 

1753 return BuildSystemCharacteristics( 

1754 out_of_source_builds="required", 

1755 ) 

1756 

1757 @classmethod 

1758 def auto_detect_build_system( 

1759 cls, 

1760 source_root: VirtualPath, 

1761 *args, 

1762 **kwargs, 

1763 ) -> bool: 

1764 return "meson.build" in source_root 

1765 

1766 @staticmethod 

1767 def _default_meson_env() -> EnvironmentModification: 

1768 replacements = { 

1769 "LC_ALL": "C.UTF-8", 

1770 } 

1771 if "DEB_PYTHON_INSTALL_LAYOUT" not in os.environ: 

1772 replacements["DEB_PYTHON_INSTALL_LAYOUT"] = "deb" 

1773 return EnvironmentModification( 

1774 replacements=tuple((k, v) for k, v in replacements.items()) 

1775 ) 

1776 

1777 @classmethod 

1778 def cmake_generator(cls, target_build_system: Literal["make", "ninja"]) -> str: 

1779 cmake_generators = { 

1780 "make": "Unix Makefiles", 

1781 "ninja": "Ninja", 

1782 } 

1783 return cmake_generators[target_build_system] 

1784 

1785 @staticmethod 

1786 def _cross_flags( 

1787 context: "BuildContext", 

1788 meson_flags: list[str], 

1789 ) -> None: 

1790 if not context.is_cross_compiling: 

1791 return 

1792 # Needs a cross-file http://mesonbuild.com/Cross-compilation.html 

1793 cross_files_dir = os.path.abspath( 

1794 generated_content_dir( 

1795 subdir_key="meson-cross-files", 

1796 ) 

1797 ) 

1798 host_arch = context.dpkg_architecture_variables.current_host_arch 

1799 cross_file = os.path.join(cross_files_dir, f"meson-cross-file-{host_arch}.conf") 

1800 if not os.path.isfile(cross_file): 

1801 subprocess.check_call( 

1802 [ 

1803 "/usr/share/meson/debcrossgen", 

1804 f"--arch={host_arch}", 

1805 f"-o{cross_file}", 

1806 ], 

1807 stdout=subprocess.DEVNULL, 

1808 env=collections.ChainMap({"LC_ALL": "C.UTF-8"}, os.environ), 

1809 ) 

1810 

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

1812 meson_flags.append(cross_file) 

1813 

1814 def configure_impl( 

1815 self, 

1816 context: "BuildContext", 

1817 manifest: "HighLevelManifest", 

1818 **kwargs, 

1819 ) -> None: 

1820 meson_version = Version( 

1821 subprocess.check_output( 

1822 ["meson", "--version"], 

1823 encoding="utf-8", 

1824 ).strip() 

1825 ) 

1826 dpkg_architecture_variables = context.dpkg_architecture_variables 

1827 

1828 meson_flags = [ 

1829 "--wrap-mode=nodownload", 

1830 "--buildtype=plain", 

1831 "--prefix=/usr", 

1832 "--sysconfdir=/etc", 

1833 "--localstatedir=/var", 

1834 f"--libdir=lib/{dpkg_architecture_variables.current_host_multiarch}", 

1835 "--auto-features=enabled", 

1836 ] 

1837 if meson_version >= Version("1.2.0"): 

1838 # There was a behavior change in Meson 1.2.0: previously 

1839 # byte-compilation wasn't supported, but since 1.2.0 it is on by 

1840 # default. We can only use this option to turn it off in versions 

1841 # where the option exists. 

1842 meson_flags.append("-Dpython.bytecompile=-1") 

1843 

1844 self._cross_flags(context, meson_flags) 

1845 

1846 if self.configure_args: 

1847 substitution = self.substitution 

1848 attr_path = self.attribute_path["configure_args"] 

1849 meson_flags.extend( 

1850 substitution.substitute(v, attr_path[i].path) 

1851 for i, v in enumerate(self.configure_args) 

1852 ) 

1853 

1854 env_mod = self._default_meson_env() 

1855 

1856 self.ensure_build_dir_exists() 

1857 source_dir_from_build_dir = self.relative_from_builddir_to_source() 

1858 

1859 with self.dump_logs_on_error("meson-logs/meson-log.txt"): 

1860 run_build_system_command( 

1861 "meson", 

1862 "setup", 

1863 source_dir_from_build_dir, 

1864 *meson_flags, 

1865 cwd=self.build_directory, 

1866 env_mod=env_mod, 

1867 ) 

1868 

1869 def build_impl( 

1870 self, 

1871 context: "BuildContext", 

1872 manifest: "HighLevelManifest", 

1873 **kwargs, 

1874 ) -> None: 

1875 self._ninja_support.run_ninja_build(context) 

1876 

1877 def test_impl( 

1878 self, 

1879 context: "BuildContext", 

1880 manifest: "HighLevelManifest", 

1881 *, 

1882 should_ignore_test_errors: bool = False, 

1883 **kwargs, 

1884 ) -> None: 

1885 env_mod = EnvironmentModification( 

1886 replacements=(("MESON_TESTTHREADS", f"{context.parallelization_limit()}"),), 

1887 ).combine(self._default_meson_env()) 

1888 meson_args = [] 

1889 if not context.is_terse_build: 

1890 meson_args.append("--verbose") 

1891 with self.dump_logs_on_error("meson-logs/testlog.txt"): 

1892 run_build_system_command( 

1893 "meson", 

1894 "test", 

1895 *meson_args, 

1896 env_mod=env_mod, 

1897 cwd=self.build_directory, 

1898 ) 

1899 

1900 def install_impl( 

1901 self, 

1902 context: "BuildContext", 

1903 manifest: "HighLevelManifest", 

1904 dest_dir: str, 

1905 **kwargs, 

1906 ) -> None: 

1907 run_build_system_command( 

1908 "meson", 

1909 "install", 

1910 "--destdir", 

1911 dest_dir, 

1912 cwd=self.build_directory, 

1913 env_mod=self._default_meson_env(), 

1914 ) 

1915 

1916 def clean_impl( 

1917 self, 

1918 context: "BuildContext", 

1919 manifest: "HighLevelManifest", 

1920 clean_helper: "CleanHelper", 

1921 **kwargs, 

1922 ) -> None: 

1923 # `debputy` will handle all the cleanup for us by virtue of "out of source build" 

1924 assert self.out_of_source_build 

1925 

1926 

1927def _add_qmake_flag(options: list[str], envvar: str, *, include_cppflags: bool) -> None: 

1928 value = os.environ.get(envvar) 

1929 if value is None: 

1930 return 

1931 if include_cppflags: 

1932 cppflags = os.environ.get("CPPFLAGS") 

1933 if cppflags: 

1934 value = f"{value} {cppflags}" 

1935 

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

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

1938 

1939 

1940class ParsedGenericQmakeBuildRuleDefinition( 

1941 OptionalInstallDirectly, 

1942 OptionalInSourceBuild, 

1943 OptionalBuildDirectory, 

1944 OptionalTestRule, 

1945): 

1946 configure_args: NotRequired[list[str]] 

1947 

1948 

1949class AbstractQmakeBuildSystemRule(StepBasedBuildSystemRule): 

1950 

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

1952 

1953 def __init__( 

1954 self, 

1955 parsed_data: "ParsedGenericQmakeBuildRuleDefinition", 

1956 attribute_path: AttributePath, 

1957 parser_context: Union[ParserContextData, "HighLevelManifest"], 

1958 ) -> None: 

1959 super().__init__(parsed_data, attribute_path, parser_context) 

1960 self.configure_args = list(parsed_data.get("configure_args", [])) 

1961 self._make_support = MakefileSupport.from_build_system(self) 

1962 

1963 @classmethod 

1964 def characteristics(cls) -> BuildSystemCharacteristics: 

1965 return BuildSystemCharacteristics( 

1966 out_of_source_builds="supported-and-default", 

1967 ) 

1968 

1969 @classmethod 

1970 def auto_detect_build_system( 

1971 cls, 

1972 source_root: VirtualPath, 

1973 *args, 

1974 **kwargs, 

1975 ) -> bool: 

1976 return any(p.name.endswith(".pro") for p in source_root.iterdir()) 

1977 

1978 @classmethod 

1979 def os_mkspec_mapping(cls) -> Mapping[str, str]: 

1980 return { 

1981 "linux": "linux-g++", 

1982 "kfreebsd": "gnukfreebsd-g++", 

1983 "hurd": "hurd-g++", 

1984 } 

1985 

1986 def qmake_command_basename(self) -> str: 

1987 raise NotImplementedError 

1988 

1989 def configure_impl( 

1990 self, 

1991 context: "BuildContext", 

1992 manifest: "HighLevelManifest", 

1993 **kwargs, 

1994 ) -> None: 

1995 

1996 configure_args = [ 

1997 "-makefile", 

1998 ] 

1999 qmake_cmd = find_source_level_build_tool( 

2000 context.dpkg_architecture_variables, 

2001 command=self.qmake_command_basename(), 

2002 required=True, 

2003 ) 

2004 

2005 if context.is_cross_compiling: 

2006 host_os = context.dpkg_architecture_variables["DEB_HOST_ARCH_OS"] 

2007 os2mkspec = self.os_mkspec_mapping() 

2008 try: 

2009 spec = os2mkspec[host_os] 

2010 except KeyError: 

2011 _error( 

2012 f'Sorry, `debputy` cannot cross build this package for "{host_os}".' 

2013 f' Missing a "DEB OS -> qmake -spec <VALUE>" mapping.' 

2014 ) 

2015 configure_args.append("-spec") 

2016 configure_args.append(spec) 

2017 

2018 _add_qmake_flag(configure_args, "CFLAGS", include_cppflags=True) 

2019 _add_qmake_flag(configure_args, "CXXFLAGS", include_cppflags=True) 

2020 _add_qmake_flag(configure_args, "LDFLAGS", include_cppflags=False) 

2021 

2022 configure_args.append("QMAKE_STRIP=:") 

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

2024 

2025 if self.configure_args: 

2026 substitution = self.substitution 

2027 attr_path = self.attribute_path["configure_args"] 

2028 configure_args.extend( 

2029 substitution.substitute(v, attr_path[i].path) 

2030 for i, v in enumerate(self.configure_args) 

2031 ) 

2032 

2033 self.ensure_build_dir_exists() 

2034 if not self.out_of_source_build: 

2035 configure_args.append(self.relative_from_builddir_to_source()) 

2036 

2037 with self.dump_logs_on_error("config.log"): 

2038 run_build_system_command( 

2039 qmake_cmd, 

2040 *configure_args, 

2041 cwd=self.build_directory, 

2042 ) 

2043 

2044 def build_impl( 

2045 self, 

2046 context: "BuildContext", 

2047 manifest: "HighLevelManifest", 

2048 **kwargs, 

2049 ) -> None: 

2050 self._make_support.run_make(context) 

2051 

2052 def test_impl( 

2053 self, 

2054 context: "BuildContext", 

2055 manifest: "HighLevelManifest", 

2056 *, 

2057 should_ignore_test_errors: bool = False, 

2058 **kwargs, 

2059 ) -> None: 

2060 limit = context.parallelization_limit(support_zero_as_unlimited=True) 

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

2062 

2063 if not context.is_terse_build: 

2064 testsuite_flags.append("--verbose") 

2065 self._make_support.run_first_existing_target_if_any( 

2066 context, 

2067 # Order is deliberately inverse compared to debhelper (#924052) 

2068 ["check", "test"], 

2069 f"TESTSUITEFLAGS={' '.join(testsuite_flags)}", 

2070 "VERBOSE=1", 

2071 ) 

2072 

2073 def install_impl( 

2074 self, 

2075 context: "BuildContext", 

2076 manifest: "HighLevelManifest", 

2077 dest_dir: str, 

2078 **kwargs, 

2079 ) -> None: 

2080 enable_parallelization = not os.path.lexists(self.build_dir_path("libtool")) 

2081 self._make_support.run_first_existing_target_if_any( 

2082 context, 

2083 ["install"], 

2084 f"DESTDIR={dest_dir}", 

2085 "AM_UPDATE_INFO_DIR=no", 

2086 enable_parallelization=enable_parallelization, 

2087 ) 

2088 

2089 def clean_impl( 

2090 self, 

2091 context: "BuildContext", 

2092 manifest: "HighLevelManifest", 

2093 clean_helper: "CleanHelper", 

2094 **kwargs, 

2095 ) -> None: 

2096 if self.out_of_source_build: 

2097 return 

2098 self._make_support.run_first_existing_target_if_any( 

2099 context, 

2100 ["distclean", "realclean", "clean"], 

2101 ) 

2102 

2103 

2104class QmakeBuildSystemRule(AbstractQmakeBuildSystemRule): 

2105 

2106 def qmake_command_basename(self) -> str: 

2107 return "qmake" 

2108 

2109 

2110class Qmake6BuildSystemRule(AbstractQmakeBuildSystemRule): 

2111 

2112 def qmake_command_basename(self) -> str: 

2113 return "qmake6" 

2114 

2115 

2116@debputy_build_system( 

2117 "make", 

2118 MakefileBuildSystemRule, 

2119 auto_detection_shadows_build_systems="debhelper", 

2120 online_reference_documentation=reference_documentation( 

2121 title="Make Build System", 

2122 description=textwrap.dedent("""\ 

2123 Run a plain `make` file with nothing else. 

2124 

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

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

2127 

2128 By default, the makefile build system assumes it should use "in-source" 

2129 build semantics. If needed be, an explicit `build-directory` can be 

2130 provided if the `Makefile` is not in the source folder but instead in 

2131 some other directory. 

2132 """), 

2133 attributes=[ 

2134 documented_attr( 

2135 "directory", 

2136 textwrap.dedent("""\ 

2137 The directory from which to run make if it is not the source root 

2138 

2139 This works like using `make -C DIRECTORY ...` (or `cd DIRECTORY && make ...`). 

2140 """), 

2141 ), 

2142 documented_attr( 

2143 "build_target", 

2144 textwrap.dedent("""\ 

2145 The target name to use for the "build" step. 

2146 

2147 If omitted, `make` will be run without any explicit target leaving it to decide 

2148 the default. 

2149 """), 

2150 ), 

2151 documented_attr( 

2152 "test_target", 

2153 textwrap.dedent("""\ 

2154 The target name to use for the "test" step. 

2155 

2156 If omitted, `make check` or `make test` will be used if it looks like `make` 

2157 will accept one of those targets. Otherwise, the step will be skipped. 

2158 """), 

2159 ), 

2160 documented_attr( 

2161 "install_target", 

2162 textwrap.dedent("""\ 

2163 The target name to use for the "install" step. 

2164 

2165 If omitted, `make install` will be used if it looks like `make` will accept that target. 

2166 Otherwise, the step will be skipped. 

2167 """), 

2168 ), 

2169 documented_attr( 

2170 "install_target_variables", 

2171 textwrap.dedent("""\ 

2172 Variables (Macros in make lingo) passed when invoking the install target. 

2173 

2174 This can be used to pass variables to the install target such as `PREFIX=/usr`. 

2175 In the `debputy` manifest, they are described as a mapping: 

2176 

2177 ``` 

2178 install_target_variables: 

2179 PREFIX: "/usr" 

2180 ``` 

2181 

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

2183 case of the variable name that the Makefile expects. 

2184 

2185 If a variable provided here clashes with a variable that `debputy` provides 

2186 by default (such as `DESTDIR` or `INSTALL`), the variable from the manifest 

2187 is used instead of the `debputy` default. 

2188 

2189 The value can be set to `null` to make sure the variable is not passed. That 

2190 is, it will suppress any value from `debputy` and let the value from `Makefile` 

2191 (if any) take effect. Using the empty string will pass the variable with no value 

2192 (such as `make ... FOO=`). This variant will still override the value of the 

2193 variable in the Makefile. 

2194 """), 

2195 ), 

2196 *docs_from( 

2197 DebputyParsedContentStandardConditional, 

2198 OptionalInstallDirectly, 

2199 OptionalTestRule, 

2200 BuildRuleParsedFormat, 

2201 ), 

2202 ], 

2203 ), 

2204) 

2205class ParsedMakeBuildRuleDefinition( 

2206 OptionalInstallDirectly, 

2207 OptionalTestRule, 

2208): 

2209 directory: NotRequired[FileSystemExactMatchRule] 

2210 build_target: NotRequired[str] 

2211 test_target: NotRequired[str] 

2212 install_target: NotRequired[str] 

2213 install_target_variables: NotRequired[dict[str, str | None]] 

2214 

2215 

2216@debputy_build_system( 

2217 "autoconf", 

2218 AutoconfBuildSystemRule, 

2219 auto_detection_shadows_build_systems=["debhelper", "make"], 

2220 online_reference_documentation=reference_documentation( 

2221 title="Autoconf Build System (`autoconf`)", 

2222 description=textwrap.dedent("""\ 

2223 Run an autoconf-based build system as the upstream build system. 

2224 

2225 This build rule will attempt to use autoreconf to update the `configure` 

2226 script before running the `configure` script if needed. Otherwise, it 

2227 follows the classic `./configure && make && make install` pattern. 

2228 

2229 The build rule uses "out of source" builds by default since it is easier 

2230 and more reliable for clean and makes it easier to support multiple 

2231 builds (that is, two or more build systems for the same source package). 

2232 This is in contract to `debhelper`, which defaults to "in source" builds 

2233 for `autoconf`. If you need that behavior, please set 

2234 `perform-in-source-build: true`. 

2235 """), 

2236 attributes=[ 

2237 documented_attr( 

2238 "configure_args", 

2239 textwrap.dedent("""\ 

2240 Arguments to be passed to the `configure` script. 

2241 """), 

2242 ), 

2243 *docs_from( 

2244 DebputyParsedContentStandardConditional, 

2245 OptionalInstallDirectly, 

2246 OptionalInSourceBuild, 

2247 OptionalBuildDirectory, 

2248 OptionalTestRule, 

2249 BuildRuleParsedFormat, 

2250 ), 

2251 ], 

2252 ), 

2253) 

2254class ParsedAutoconfBuildRuleDefinition( 

2255 OptionalInstallDirectly, 

2256 OptionalInSourceBuild, 

2257 OptionalBuildDirectory, 

2258 OptionalTestRule, 

2259): 

2260 configure_args: NotRequired[list[str]] 

2261 

2262 

2263@debputy_build_system( 

2264 "cmake", 

2265 CMakeBuildSystemRule, 

2266 auto_detection_shadows_build_systems=["debhelper", "make"], 

2267 online_reference_documentation=reference_documentation( 

2268 title="CMake Build System (`cmake`)", 

2269 description=textwrap.dedent("""\ 

2270 Run an cmake-based build system as the upstream build system. 

2271 

2272 The build rule uses "out of source" builds. 

2273 """), 

2274 attributes=[ 

2275 documented_attr( 

2276 "configure_args", 

2277 textwrap.dedent("""\ 

2278 Arguments to be passed to the `cmake` command. 

2279 """), 

2280 ), 

2281 documented_attr( 

2282 "target_build_system", 

2283 textwrap.dedent("""\ 

2284 Which build system should `cmake` should delegate the build system to. 

2285 In `cmake` terms, this is a Generator (the `-G` option). 

2286 

2287 Supported options are: 

2288 * `make` - GNU Make 

2289 * `ninja` - Ninja 

2290 """), 

2291 ), 

2292 *docs_from( 

2293 DebputyParsedContentStandardConditional, 

2294 OptionalInstallDirectly, 

2295 OptionalBuildDirectory, 

2296 OptionalTestRule, 

2297 BuildRuleParsedFormat, 

2298 ), 

2299 ], 

2300 ), 

2301) 

2302class ParsedCMakeBuildRuleDefinition( 

2303 OptionalInstallDirectly, 

2304 OptionalBuildDirectory, 

2305 OptionalTestRule, 

2306): 

2307 configure_args: NotRequired[list[str]] 

2308 target_build_system: Literal["make", "ninja"] 

2309 

2310 

2311@debputy_build_system( 

2312 "meson", 

2313 MesonBuildSystemRule, 

2314 auto_detection_shadows_build_systems=["debhelper", "make"], 

2315 online_reference_documentation=reference_documentation( 

2316 title="Meson Build System (`meson`)", 

2317 description=textwrap.dedent("""\ 

2318 Run an meson-based build system as the upstream build system. 

2319 

2320 The build rule uses "out of source" builds. 

2321 """), 

2322 attributes=[ 

2323 documented_attr( 

2324 "configure_args", 

2325 textwrap.dedent("""\ 

2326 Arguments to be passed to the `meson` command. 

2327 """), 

2328 ), 

2329 *docs_from( 

2330 DebputyParsedContentStandardConditional, 

2331 OptionalInstallDirectly, 

2332 OptionalBuildDirectory, 

2333 OptionalTestRule, 

2334 BuildRuleParsedFormat, 

2335 ), 

2336 ], 

2337 ), 

2338) 

2339class ParsedMesonBuildRuleDefinition( 

2340 OptionalInstallDirectly, 

2341 OptionalBuildDirectory, 

2342 OptionalTestRule, 

2343): 

2344 configure_args: NotRequired[list[str]] 

2345 

2346 

2347@debputy_build_system( 

2348 "perl-build", 

2349 PerlBuildBuildSystemRule, 

2350 auto_detection_shadows_build_systems=[ 

2351 "debhelper", 

2352 "make", 

2353 "perl-makemaker", 

2354 ], 

2355 online_reference_documentation=reference_documentation( 

2356 title='Perl "Build.PL" Build System (`perl-build`)', 

2357 description=textwrap.dedent("""\ 

2358 Build using the `Build.PL` Build system used by some Perl packages. 

2359 

2360 This build rule will attempt to use the `Build.PL` script to build the 

2361 upstream code. 

2362 """), 

2363 attributes=[ 

2364 documented_attr( 

2365 "configure_args", 

2366 textwrap.dedent("""\ 

2367 Arguments to be passed to the `Build.PL` script. 

2368 """), 

2369 ), 

2370 *docs_from( 

2371 DebputyParsedContentStandardConditional, 

2372 OptionalInstallDirectly, 

2373 OptionalTestRule, 

2374 BuildRuleParsedFormat, 

2375 ), 

2376 ], 

2377 ), 

2378) 

2379class ParsedPerlBuildBuildRuleDefinition( 

2380 OptionalInstallDirectly, 

2381 OptionalTestRule, 

2382): 

2383 configure_args: NotRequired[list[str]] 

2384 

2385 

2386@debputy_build_system( 

2387 "debhelper", 

2388 DebhelperBuildSystemRule, 

2389 online_reference_documentation=reference_documentation( 

2390 title="Debhelper Build System (`debhelper`)", 

2391 description=textwrap.dedent("""\ 

2392 Delegate to a debhelper provided build system 

2393 

2394 This build rule will attempt to use the `dh_auto_*` tools to build the 

2395 upstream code. By default, `dh_auto_*` will use auto-detection to determine 

2396 which build system they will use. This can be overridden by the 

2397 `dh-build-system` attribute. 

2398 """), 

2399 attributes=[ 

2400 documented_attr( 

2401 "dh_build_system", 

2402 textwrap.dedent("""\ 

2403 Which debhelper build system to use. This attribute is passed to 

2404 the `dh_auto_*` commands as the `-S` parameter, so any value valid 

2405 for that will be accepted. 

2406 

2407 Note that many debhelper build systems require extra build 

2408 dependencies before they can be used. Please consult the documentation 

2409 of the relevant debhelper build system for details. 

2410 """), 

2411 ), 

2412 documented_attr( 

2413 "configure_args", 

2414 textwrap.dedent("""\ 

2415 Arguments to be passed to underlying configuration command 

2416 (via `dh_auto_configure -- <configure-args`). 

2417 """), 

2418 ), 

2419 *docs_from( 

2420 DebputyParsedContentStandardConditional, 

2421 OptionalInstallDirectly, 

2422 OptionalBuildDirectory, 

2423 OptionalTestRule, 

2424 BuildRuleParsedFormat, 

2425 ), 

2426 ], 

2427 ), 

2428) 

2429class ParsedDebhelperBuildRuleDefinition( 

2430 OptionalInstallDirectly, 

2431 OptionalBuildDirectory, 

2432 OptionalTestRule, 

2433): 

2434 configure_args: NotRequired[list[str]] 

2435 dh_build_system: NotRequired[str] 

2436 

2437 

2438@debputy_build_system( 

2439 "perl-makemaker", 

2440 PerlMakeMakerBuildSystemRule, 

2441 auto_detection_shadows_build_systems=[ 

2442 "debhelper", 

2443 "make", 

2444 ], 

2445 online_reference_documentation=reference_documentation( 

2446 title='Perl "MakeMaker" Build System (`perl-makemaker`)', 

2447 description=textwrap.dedent("""\ 

2448 Build using the "MakeMaker" Build system used by some Perl packages. 

2449 

2450 This build rule will attempt to use the `Makefile.PL` script to build the 

2451 upstream code. 

2452 """), 

2453 attributes=[ 

2454 documented_attr( 

2455 "configure_args", 

2456 textwrap.dedent("""\ 

2457 Arguments to be passed to the `Makefile.PL` script. 

2458 """), 

2459 ), 

2460 *docs_from( 

2461 DebputyParsedContentStandardConditional, 

2462 OptionalInstallDirectly, 

2463 OptionalTestRule, 

2464 BuildRuleParsedFormat, 

2465 ), 

2466 ], 

2467 ), 

2468) 

2469class ParsedPerlMakeMakerBuildRuleDefinition( 

2470 OptionalInstallDirectly, 

2471 OptionalTestRule, 

2472): 

2473 configure_args: NotRequired[list[str]] 

2474 

2475 

2476@debputy_build_system( 

2477 "qmake", 

2478 QmakeBuildSystemRule, 

2479 auto_detection_shadows_build_systems=[ 

2480 "debhelper", 

2481 "make", 

2482 # Open question, should this shadow "qmake6" and later? 

2483 ], 

2484 online_reference_documentation=reference_documentation( 

2485 title='QT "qmake" Build System (`qmake`)', 

2486 description=textwrap.dedent("""\ 

2487 Build using the "qmake" by QT. 

2488 """), 

2489 attributes=[ 

2490 documented_attr( 

2491 "configure_args", 

2492 textwrap.dedent("""\ 

2493 Arguments to be passed to the `qmake` command. 

2494 """), 

2495 ), 

2496 *docs_from( 

2497 DebputyParsedContentStandardConditional, 

2498 OptionalInstallDirectly, 

2499 OptionalInSourceBuild, 

2500 OptionalBuildDirectory, 

2501 OptionalTestRule, 

2502 BuildRuleParsedFormat, 

2503 ), 

2504 ], 

2505 ), 

2506) 

2507class ParsedQmakeBuildRuleDefinition(ParsedGenericQmakeBuildRuleDefinition): 

2508 pass 

2509 

2510 

2511@debputy_build_system( 

2512 "qmake6", 

2513 Qmake6BuildSystemRule, 

2514 auto_detection_shadows_build_systems=[ 

2515 "debhelper", 

2516 "make", 

2517 ], 

2518 online_reference_documentation=reference_documentation( 

2519 title='QT "qmake6" Build System (`qmake6`)', 

2520 description=textwrap.dedent("""\ 

2521 Build using the "qmake6" from the `qmake6` package. This is like the `qmake` system 

2522 but is specifically for QT6. 

2523 """), 

2524 attributes=[ 

2525 documented_attr( 

2526 "configure_args", 

2527 textwrap.dedent("""\ 

2528 Arguments to be passed to the `qmake6` command. 

2529 """), 

2530 ), 

2531 *docs_from( 

2532 DebputyParsedContentStandardConditional, 

2533 OptionalInstallDirectly, 

2534 OptionalInSourceBuild, 

2535 OptionalBuildDirectory, 

2536 OptionalTestRule, 

2537 BuildRuleParsedFormat, 

2538 ), 

2539 ], 

2540 ), 

2541) 

2542class ParsedQmake6BuildRuleDefinition(ParsedGenericQmakeBuildRuleDefinition): 

2543 pass 

2544 

2545 

2546def _parse_default_environment( 

2547 _name: str, 

2548 parsed_data: EnvironmentSourceFormat, 

2549 attribute_path: AttributePath, 

2550 parser_context: ParserContextData, 

2551) -> ManifestProvidedBuildEnvironment: 

2552 return ManifestProvidedBuildEnvironment.from_environment_definition( 

2553 parsed_data, 

2554 attribute_path, 

2555 parser_context, 

2556 is_default=True, 

2557 ) 

2558 

2559 

2560def _parse_build_environments( 

2561 _name: str, 

2562 parsed_data: list[NamedEnvironmentSourceFormat], 

2563 attribute_path: AttributePath, 

2564 parser_context: ParserContextData, 

2565) -> list[ManifestProvidedBuildEnvironment]: 

2566 return [ 

2567 ManifestProvidedBuildEnvironment.from_environment_definition( 

2568 value, 

2569 attribute_path[idx], 

2570 parser_context, 

2571 is_default=False, 

2572 ) 

2573 for idx, value in enumerate(parsed_data) 

2574 ] 

2575 

2576 

2577def _handle_build_rules( 

2578 _name: str, 

2579 parsed_data: list[BuildRule], 

2580 _attribute_path: AttributePath, 

2581 _parser_context: ParserContextData, 

2582) -> list[BuildRule]: 

2583 return parsed_data