Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Userspace: add support for adding environment variables during build #22887

Merged
merged 41 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
d6bfe13
Attempt to get env parsing sorted.
tzarc Jan 8, 2024
dec71d7
Add support for extra args to BuildTarget.
tzarc Jan 8, 2024
dc1e8ca
Initial propagation of env vars.
tzarc Jan 11, 2024
85d0be4
Allow for same board, different env vars.
tzarc Jan 11, 2024
9dcf283
Fixup userspace-list expansion.
tzarc Jan 11, 2024
cb9bec3
Consistency.
tzarc Jan 11, 2024
1cf0d08
Move versioned build target schema definitions to userspace repo sche…
tzarc Jan 11, 2024
3585543
Merge remote-tracking branch 'upstream/master' into userspace-env
tzarc Jan 11, 2024
3a79b4c
Save/load env vars as object instead of array.
tzarc Jan 12, 2024
7038050
`qmk userspace-list` updates.
tzarc Jan 12, 2024
2de1056
More dict conversions.
tzarc Jan 12, 2024
1bef97e
`qmk pytest`
tzarc Jan 12, 2024
3192640
Schema cleanup.
tzarc Jan 12, 2024
ccbc183
Target filename manipulation... next step INTERMEDIATE_OUTPUT.
tzarc Jan 12, 2024
de3af67
First stab at building intermediate directory based on variables that…
tzarc Jan 12, 2024
07ec161
Merge remote-tracking branch 'upstream/master' into userspace-env
tzarc Jan 19, 2024
e5cedf3
Rely on makefiles to generate INTERMEDIATE_OUTPUT for the build.
tzarc Jan 19, 2024
bfa1d80
Let's have a stab at forcing community layouts.
tzarc Jan 19, 2024
cad628f
Fix.
tzarc Jan 19, 2024
4828ee4
Fix.
tzarc Jan 19, 2024
616f4c9
Fix.
tzarc Jan 19, 2024
5ce1f4e
More force_layout hot potato.
tzarc Jan 19, 2024
b4d7b79
Naming is hard.
tzarc Jan 19, 2024
6f9babf
Merge remote-tracking branch 'upstream/master' into userspace-env
tzarc Mar 9, 2024
c995d35
License years.
tzarc Mar 9, 2024
8d1f9cf
Merge remote-tracking branch 'upstream/master' into userspace-env
tzarc Apr 21, 2024
0bbd470
More conversion stuff.
tzarc Apr 22, 2024
9cb0800
Merge remote-tracking branch 'upstream/master' into userspace-env
tzarc Apr 23, 2024
a695c73
KeyboardKeymapDesc
tzarc Apr 23, 2024
91466e8
Apply original keyboard/keymap
tzarc Apr 23, 2024
bb60311
Apply original keyboard/keymap
tzarc Apr 23, 2024
05b1852
Apply original keyboard/keymap
tzarc Apr 23, 2024
d51f58b
Simplification pass.
tzarc Apr 23, 2024
0abbbd1
Fixup
tzarc Apr 23, 2024
3c9ddd3
Simplification pass.
tzarc Apr 23, 2024
c650857
Simplification pass.
tzarc Apr 23, 2024
112b4a2
Fixup
tzarc Apr 23, 2024
c4b70df
simplification
tzarc Apr 26, 2024
dc8b4b1
simplification
tzarc Apr 26, 2024
09d1ce7
`qmk pytest`, dry-run
tzarc Apr 26, 2024
53bc15e
shell quotes
tzarc Apr 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions builddefs/build_keyboard.mk
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,16 @@ ifeq ($(strip $(DUMP_CI_METADATA)),yes)
endif

# Force expansion
TARGET := $(TARGET)
override TARGET := $(TARGET)
$(info TARGET=$(TARGET))

ifneq ($(FORCE_LAYOUT),)
TARGET := $(TARGET)_$(FORCE_LAYOUT)
override TARGET := $(TARGET)_$(FORCE_LAYOUT)
$(info TARGET=$(TARGET))
endif
ifneq ($(CONVERT_TO),)
override TARGET := $(TARGET)_$(CONVERT_TO)
$(info TARGET=$(TARGET))
endif

# Object files and generated keymap directory
Expand All @@ -58,9 +64,6 @@ ifdef SKIP_GIT
VERSION_H_FLAGS += --skip-git
endif

# Generate the board's version.h file.
$(shell $(QMK_BIN) generate-version-h $(VERSION_H_FLAGS) -q -o $(INTERMEDIATE_OUTPUT)/src/version.h)

# Determine which subfolders exist.
KEYBOARD_FOLDER_PATH_1 := $(KEYBOARD)
KEYBOARD_FOLDER_PATH_2 := $(patsubst %/,%,$(dir $(KEYBOARD_FOLDER_PATH_1)))
Expand Down Expand Up @@ -218,6 +221,9 @@ endif

include $(BUILDDEFS_PATH)/converters.mk

# Generate the board's version.h file.
$(shell $(QMK_BIN) generate-version-h $(VERSION_H_FLAGS) -q -o $(INTERMEDIATE_OUTPUT)/src/version.h)

MCU_ORIG := $(MCU)
include $(wildcard $(PLATFORM_PATH)/*/mcu_selection.mk)

Expand Down
3 changes: 0 additions & 3 deletions builddefs/converters.mk
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ ifneq ($(CONVERT_TO),)

PLATFORM_KEY = $(shell echo $(CONVERTER) | cut -d "/" -f2)

# force setting as value can be from environment
override TARGET := $(TARGET)_$(CONVERT_TO)

# Configure any defaults
OPT_DEFS += -DCONVERT_TO_$(shell echo $(CONVERT_TO) | tr '[:lower:]' '[:upper:]')
OPT_DEFS += -DCONVERTER_TARGET=\"$(CONVERT_TO)\"
Expand Down
23 changes: 17 additions & 6 deletions data/schemas/definitions.jsonschema
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,6 @@
"type": "object",
"additionalProperties": {"type": "boolean"}
},
"build_target": {
"oneOf": [
{"$ref": "#/keyboard_keymap_tuple"},
{"$ref": "#/json_file_path"}
]
},
"filename": {
"type": "string",
"minLength": 1,
Expand Down Expand Up @@ -52,6 +46,19 @@
{"$ref": "#/keyboard"},
{"$ref": "#/filename"}
],
"minItems": 2,
"maxItems": 2,
"unevaluatedItems": false
},
"keyboard_keymap_env": {
"type": "array",
"prefixItems": [
{"$ref": "#/keyboard"},
{"$ref": "#/filename"},
{"$ref": "#/kvp_object"}
],
"minItems": 3,
"maxItems": 3,
"unevaluatedItems": false
},
"keycode": {
Expand Down Expand Up @@ -86,6 +93,10 @@
"maxLength": 7,
"pattern": "^[A-Z][A-Zs_0-9]*$"
},
"kvp_object": {
"type": "object",
"additionalProperties": {"type": "string"}
},
"layout_macro": {
"oneOf": [
{
Expand Down
10 changes: 9 additions & 1 deletion data/schemas/user_repo_v1.jsonschema
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
"$id": "qmk.user_repo.v1",
"title": "User Repository Information",
"type": "object",
"definitions": {
"build_target": {
"oneOf": [
{"$ref": "qmk.definitions.v1#/keyboard_keymap_tuple"},
{"$ref": "qmk.definitions.v1#/json_file_path"}
]
},
},
"required": [
"userspace_version",
"build_targets"
Expand All @@ -15,7 +23,7 @@
"build_targets": {
"type": "array",
"items": {
"$ref": "qmk.definitions.v1#/build_target"
"$ref": "#/definitions/build_target"
}
}
}
Expand Down
31 changes: 31 additions & 0 deletions data/schemas/user_repo_v1_1.jsonschema
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema#",
"$id": "qmk.user_repo.v1_1",
"title": "User Repository Information",
"type": "object",
"definitions": {
"build_target": {
"oneOf": [
{"$ref": "qmk.definitions.v1#/keyboard_keymap_tuple"},
{"$ref": "qmk.definitions.v1#/keyboard_keymap_env"},
{"$ref": "qmk.definitions.v1#/json_file_path"}
]
},
},
"required": [
"userspace_version",
"build_targets"
],
"properties": {
"userspace_version": {
"type": "string",
"enum": ["1.1"]
},
"build_targets": {
"type": "array",
"items": {
"$ref": "#/definitions/build_target"
}
}
}
}
107 changes: 72 additions & 35 deletions lib/python/qmk/build_targets.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Copyright 2023 Nick Brassel (@tzarc)
# Copyright 2023-2024 Nick Brassel (@tzarc)
# SPDX-License-Identifier: GPL-2.0-or-later
import json
import shutil
from typing import List, Union
from typing import Dict, List, Union
from pathlib import Path
from dotty_dict import dotty, Dotty
from milc import cli
Expand All @@ -13,6 +13,9 @@
from qmk.keymap import locate_keymap
from qmk.path import is_under_qmk_firmware, is_under_qmk_userspace

# These must be kept in the order in which they're applied to $(TARGET) in the makefiles in order to ensure consistency.
TARGET_FILENAME_MODIFIERS = ['FORCE_LAYOUT', 'CONVERT_TO']


class BuildTarget:
def __init__(self, keyboard: str, keymap: str, json: Union[dict, Dotty] = None):
Expand All @@ -22,25 +25,25 @@ def __init__(self, keyboard: str, keymap: str, json: Union[dict, Dotty] = None):
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._extra_args = {}
self._json = json.to_dict() if isinstance(json, Dotty) else json

def __str__(self):
return f'{self.keyboard}:{self.keymap}'

def __repr__(self):
if len(self._extra_args.items()) > 0:
return f'BuildTarget(keyboard={self.keyboard}, keymap={self.keymap}, extra_args={json.dumps(self._extra_args, sort_keys=True)})'
return f'BuildTarget(keyboard={self.keyboard}, keymap={self.keymap})'

def __lt__(self, __value: object) -> bool:
return self.__repr__() < __value.__repr__()

def __eq__(self, __value: object) -> bool:
if not isinstance(__value, BuildTarget):
return False
return self.__repr__() == __value.__repr__()

def __ne__(self, __value: object) -> bool:
return not self.__eq__(__value)

def __hash__(self) -> int:
return self.__repr__().__hash__()

Expand Down Expand Up @@ -72,7 +75,34 @@ def json(self) -> dict:
def dotty(self) -> Dotty:
return dotty(self.json)

def _common_make_args(self, dry_run: bool = False, build_target: str = None):
@property
def extra_args(self) -> Dict[str, str]:
return {k: v for k, v in self._extra_args.items()}

@extra_args.setter
def extra_args(self, ex_args: Dict[str, str]):
if ex_args is not None and isinstance(ex_args, dict):
self._extra_args = {k: v for k, v in ex_args.items()}

def target_name(self, **env_vars) -> str:
# Work out the intended target name
target = f'{self._keyboard_safe}_{self.keymap}'
vars = self._all_vars(**env_vars)
for modifier in TARGET_FILENAME_MODIFIERS:
if modifier in vars:
target += f"_{vars[modifier]}"
return target

def _all_vars(self, **env_vars) -> Dict[str, str]:
vars = {k: v for k, v in env_vars.items()}
for k, v in self._extra_args.items():
vars[k] = v
return vars

def _intermediate_output(self, **env_vars) -> Path:
return Path(f'{INTERMEDIATE_OUTPUT_PREFIX}{self.target_name(**env_vars)}')

def _common_make_args(self, dry_run: bool = False, build_target: str = None, **env_vars):
compile_args = [
find_make(),
*get_make_parallel_args(self._parallel),
Expand All @@ -98,14 +128,17 @@ def _common_make_args(self, dry_run: bool = False, build_target: str = None):
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'TARGET={self._keyboard_safe}_{self.keymap}', # don't use self.target_name() here, it's rebuilt on the makefile side
f'VERBOSE={verbose}',
f'COLOR={color}',
'SILENT=false',
'QMK_BIN="qmk"',
])

vars = self._all_vars(**env_vars)
for k, v in vars.items():
compile_args.append(f'{k}={v}')

return compile_args

def prepare_build(self, build_target: str = None, dry_run: bool = False, **env_vars) -> None:
Expand Down Expand Up @@ -150,6 +183,8 @@ def __init__(self, keyboard: str, keymap: str, json: dict = None):
super().__init__(keyboard=keyboard, keymap=keymap, json=json)

def __repr__(self):
if len(self._extra_args.items()) > 0:
return f'KeyboardKeymapTarget(keyboard={self.keyboard}, keymap={self.keymap}, extra_args={self._extra_args})'
return f'KeyboardKeymapTarget(keyboard={self.keyboard}, keymap={self.keymap})'

def _load_json(self):
Expand All @@ -159,15 +194,13 @@ def prepare_build(self, build_target: str = None, dry_run: bool = False, **env_v
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}')
compile_args = self._common_make_args(dry_run=dry_run, build_target=build_target, **env_vars)

# Need to override the keymap path if the keymap is a userspace directory.
# This also ensures keyboard aliases as per `keyboard_aliases.hjson` still work if the userspace has the keymap
# in an equivalent historical location.
keymap_location = locate_keymap(self.keyboard, self.keymap)
vars = self._all_vars(**env_vars)
keymap_location = locate_keymap(self.keyboard, self.keymap, force_layout=vars.get('FORCE_LAYOUT'))
if is_under_qmk_userspace(keymap_location) and not is_under_qmk_firmware(keymap_location):
keymap_directory = keymap_location.parent
compile_args.extend([
Expand Down Expand Up @@ -196,47 +229,51 @@ def __init__(self, json_path):

super().__init__(keyboard=json['keyboard'], keymap=json['keymap'], json=json)

self._keymap_json = self._generated_files_path / 'keymap.json'

def __repr__(self):
if len(self._extra_args.items()) > 0:
return f'JsonKeymapTarget(keyboard={self.keyboard}, keymap={self.keymap}, path={self.json_path}, extra_args={self._extra_args})'
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:
intermediate_output = self._intermediate_output(**env_vars)
generated_files_path = intermediate_output / 'src'
keymap_json = generated_files_path / 'keymap.json'

if self._clean:
if self._intermediate_output.exists():
shutil.rmtree(self._intermediate_output)
if intermediate_output.exists():
shutil.rmtree(intermediate_output)

# begin with making the deepest folder in the tree
self._generated_files_path.mkdir(exist_ok=True, parents=True)
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 keymap_json.exists():
old_content = json.dumps(json.loads(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')
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 = self._common_make_args(dry_run=dry_run, build_target=build_target, **env_vars)
intermediate_output = self._intermediate_output(**env_vars)
generated_files_path = intermediate_output / 'src'
keymap_json = generated_files_path / 'keymap.json'
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}',
f'MAIN_KEYMAP_PATH_1={intermediate_output}',
f'MAIN_KEYMAP_PATH_2={intermediate_output}',
f'MAIN_KEYMAP_PATH_3={intermediate_output}',
f'MAIN_KEYMAP_PATH_4={intermediate_output}',
f'MAIN_KEYMAP_PATH_5={intermediate_output}',
f'KEYMAP_JSON={keymap_json}',
f'KEYMAP_PATH={generated_files_path}',
])

for key, value in env_vars.items():
compile_args.append(f'{key}={value}')

return compile_args
9 changes: 8 additions & 1 deletion lib/python/qmk/cli/format/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,18 @@ def _detect_json_format(file, json_data):
"""
json_encoder = None
try:
validate(json_data, 'qmk.user_repo.v1')
validate(json_data, 'qmk.user_repo.v1_1')
json_encoder = UserspaceJSONEncoder
except ValidationError:
pass

if json_encoder is None:
try:
validate(json_data, 'qmk.user_repo.v1')
json_encoder = UserspaceJSONEncoder
except ValidationError:
pass

if json_encoder is None:
try:
validate(json_data, 'qmk.keyboard.v1')
Expand Down
Loading
Loading