qmk_firmware/lib/python/qmk/build_targets.py
Sergey Vlasov 7e27d72cbc
Fix qmk find failure due to circular imports (#22523)
There was an import cycle in the Python modules:

  - `qmk.build_targets` imported `qmk.cli.generate.compilation_database`;
  - importing `qmk.cli.generate.compilation_database` requires
    initializing `qmk.cli` first;
  - the initialization of `qmk.cli` imported the modules for all CLI
    commands;
  - `qmk.cli.compile` imported `qmk.build_targets`.

This cycle did not matter in most cases, because `qmk.cli` was imported
first, and in that case importing `qmk.cli.generate.compilation_database`
did not trigger the initialization of `qmk.cli` again.  However, there was
one corner case when `qmk.bulld_targets` was getting imported first:

  - The `qmk find` command uses the `multiprocessing` module.
  - The `multiprocessing` module uses the `spawn` start method on macOS
    and Windows.
  - When the `spawn` method is used, the child processes initialize
    without any Python modules loaded, and the required modules are loaded
    on demand by the `pickle` module when receiving the serialized objects
    from the main process.

The result was that the `qmk find` command did not work properly on macOS
(and probably Windows too); it reported exceptions like this:

    ImportError: cannot import name 'KeyboardKeymapBuildTarget' from partially initialized module 'qmk.build_targets' (most likely due to a circular import)

Moving the offending `qmk.cli.generate.compilation_database` import into
the method which actually uses it fixes the problem.
2023-11-21 15:05:29 +00:00

212 lines
8.2 KiB
Python

# Copyright 2023 Nick Brassel (@tzarc)
# SPDX-License-Identifier: GPL-2.0-or-later
import json
import shutil
from typing import List, Union
from pathlib import Path
from dotty_dict import dotty, Dotty
from milc import cli
from qmk.constants import QMK_FIRMWARE, INTERMEDIATE_OUTPUT_PREFIX
from qmk.commands import find_make, get_make_parallel_args, parse_configurator_json
from qmk.keyboard import keyboard_folder
from qmk.info import keymap_json
class BuildTarget:
def __init__(self, keyboard: str, keymap: str, json: Union[dict, Dotty] = None):
self._keyboard = keyboard_folder(keyboard)
self._keyboard_safe = self._keyboard.replace('/', '_')
self._keymap = keymap
self._parallel = 1
self._clean = False
self._compiledb = False
self._target = f'{self._keyboard_safe}_{self.keymap}'
self._intermediate_output = Path(f'{INTERMEDIATE_OUTPUT_PREFIX}{self._target}')
self._generated_files_path = self._intermediate_output / 'src'
self._json = json.to_dict() if isinstance(json, Dotty) else json
def __str__(self):
return f'{self.keyboard}:{self.keymap}'
def __repr__(self):
return f'BuildTarget(keyboard={self.keyboard}, keymap={self.keymap})'
def configure(self, parallel: int = None, clean: bool = None, compiledb: bool = None) -> None:
if parallel is not None:
self._parallel = parallel
if clean is not None:
self._clean = clean
if compiledb is not None:
self._compiledb = compiledb
@property
def keyboard(self) -> str:
return self._keyboard
@property
def keymap(self) -> str:
return self._keymap
@property
def json(self) -> dict:
if not self._json:
self._load_json()
if not self._json:
return {}
return self._json
@property
def dotty(self) -> Dotty:
return dotty(self.json)
def _common_make_args(self, dry_run: bool = False, build_target: str = None):
compile_args = [
find_make(),
*get_make_parallel_args(self._parallel),
'-r',
'-R',
'-f',
'builddefs/build_keyboard.mk',
]
if not cli.config.general.verbose:
compile_args.append('-s')
verbose = 'true' if cli.config.general.verbose else 'false'
color = 'true' if cli.config.general.color else 'false'
if dry_run:
compile_args.append('-n')
if build_target:
compile_args.append(build_target)
compile_args.extend([
f'KEYBOARD={self.keyboard}',
f'KEYMAP={self.keymap}',
f'KEYBOARD_FILESAFE={self._keyboard_safe}',
f'TARGET={self._target}',
f'INTERMEDIATE_OUTPUT={self._intermediate_output}',
f'VERBOSE={verbose}',
f'COLOR={color}',
'SILENT=false',
'QMK_BIN="qmk"',
])
return compile_args
def prepare_build(self, build_target: str = None, dry_run: bool = False, **env_vars) -> None:
raise NotImplementedError("prepare_build() not implemented in base class")
def compile_command(self, build_target: str = None, dry_run: bool = False, **env_vars) -> List[str]:
raise NotImplementedError("compile_command() not implemented in base class")
def generate_compilation_database(self, build_target: str = None, skip_clean: bool = False, **env_vars) -> None:
self.prepare_build(build_target=build_target, **env_vars)
command = self.compile_command(build_target=build_target, dry_run=True, **env_vars)
from qmk.cli.generate.compilation_database import write_compilation_database # Lazy load due to circular references
write_compilation_database(command=command, output_path=QMK_FIRMWARE / 'compile_commands.json', skip_clean=skip_clean, **env_vars)
def compile(self, build_target: str = None, dry_run: bool = False, **env_vars) -> None:
if self._clean or self._compiledb:
command = [find_make(), "clean"]
if dry_run:
command.append('-n')
cli.log.info('Cleaning with {fg_cyan}%s', ' '.join(command))
cli.run(command, capture_output=False)
if self._compiledb and not dry_run:
self.generate_compilation_database(build_target=build_target, skip_clean=True, **env_vars)
self.prepare_build(build_target=build_target, dry_run=dry_run, **env_vars)
command = self.compile_command(build_target=build_target, **env_vars)
cli.log.info('Compiling keymap with {fg_cyan}%s', ' '.join(command))
if not dry_run:
cli.echo('\n')
ret = cli.run(command, capture_output=False)
if ret.returncode:
return ret.returncode
class KeyboardKeymapBuildTarget(BuildTarget):
def __init__(self, keyboard: str, keymap: str, json: dict = None):
super().__init__(keyboard=keyboard, keymap=keymap, json=json)
def __repr__(self):
return f'KeyboardKeymapTarget(keyboard={self.keyboard}, keymap={self.keymap})'
def _load_json(self):
self._json = keymap_json(self.keyboard, self.keymap)
def prepare_build(self, build_target: str = None, dry_run: bool = False, **env_vars) -> None:
pass
def compile_command(self, build_target: str = None, dry_run: bool = False, **env_vars) -> List[str]:
compile_args = self._common_make_args(dry_run=dry_run, build_target=build_target)
for key, value in env_vars.items():
compile_args.append(f'{key}={value}')
return compile_args
class JsonKeymapBuildTarget(BuildTarget):
def __init__(self, json_path):
if isinstance(json_path, Path):
self.json_path = json_path
else:
self.json_path = None
json = parse_configurator_json(json_path) # Will load from stdin if provided
# In case the user passes a keymap.json from a keymap directory directly to the CLI.
# e.g.: qmk compile - < keyboards/clueboard/california/keymaps/default/keymap.json
json["keymap"] = json.get("keymap", "default_json")
super().__init__(keyboard=json['keyboard'], keymap=json['keymap'], json=json)
self._keymap_json = self._generated_files_path / 'keymap.json'
def __repr__(self):
return f'JsonKeymapTarget(keyboard={self.keyboard}, keymap={self.keymap}, path={self.json_path})'
def _load_json(self):
pass # Already loaded in constructor
def prepare_build(self, build_target: str = None, dry_run: bool = False, **env_vars) -> None:
if self._clean:
if self._intermediate_output.exists():
shutil.rmtree(self._intermediate_output)
# begin with making the deepest folder in the tree
self._generated_files_path.mkdir(exist_ok=True, parents=True)
# Compare minified to ensure consistent comparison
new_content = json.dumps(self.json, separators=(',', ':'))
if self._keymap_json.exists():
old_content = json.dumps(json.loads(self._keymap_json.read_text(encoding='utf-8')), separators=(',', ':'))
if old_content == new_content:
new_content = None
# Write the keymap.json file if different so timestamps are only updated
# if the content changes -- running `make` won't treat it as modified.
if new_content:
self._keymap_json.write_text(new_content, encoding='utf-8')
def compile_command(self, build_target: str = None, dry_run: bool = False, **env_vars) -> List[str]:
compile_args = self._common_make_args(dry_run=dry_run, build_target=build_target)
compile_args.extend([
f'MAIN_KEYMAP_PATH_1={self._intermediate_output}',
f'MAIN_KEYMAP_PATH_2={self._intermediate_output}',
f'MAIN_KEYMAP_PATH_3={self._intermediate_output}',
f'MAIN_KEYMAP_PATH_4={self._intermediate_output}',
f'MAIN_KEYMAP_PATH_5={self._intermediate_output}',
f'KEYMAP_JSON={self._keymap_json}',
f'KEYMAP_PATH={self._generated_files_path}',
])
for key, value in env_vars.items():
compile_args.append(f'{key}={value}')
return compile_args