QMK CLI Environment bootstrapper (#25038)

Co-authored-by: Joel Challis <git@zvecr.com>
Co-authored-by: Pascal Getreuer <getreuer@google.com>
This commit is contained in:
Nick Brassel 2025-11-28 00:36:49 +11:00 committed by GitHub
parent 594558ec7b
commit 9c2ca00074
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1033 additions and 91 deletions

251
.github/workflows/bootstrap_testing.yml vendored Normal file
View file

@ -0,0 +1,251 @@
name: Bootstrap Script Testing
on:
push:
branches: [bootstrap]
paths:
- "util/env-bootstrap.sh"
- ".github/workflows/bootstrap_testing.yml"
- "lib/python/**"
pull_request:
branches: [master, develop, xap]
paths:
- "util/env-bootstrap.sh"
- ".github/workflows/bootstrap_testing.yml"
- "lib/python/**"
workflow_dispatch:
permissions:
contents: read
jobs:
bootstrap-test-linux:
name: Bootstrap (Linux)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
distribution:
# Ubuntu/Debian based
- debian:11
- debian:12
- debian:13
- ubuntu:20.04
- ubuntu:22.04
- ubuntu:24.04
# RHEL/CentOS/Fedora based
- fedora:41
- fedora:42
- fedora:43
- rockylinux:8
- rockylinux:9
- rockylinux/rockylinux:10
- almalinux:8
- almalinux:9
- almalinux:10
# OpenSUSE based (we skip Tumbleweed as it has issues with package versions between pattern installs and other dependencies preinstalled into the base container)
- opensuse/leap:latest
# Gentoo-based
- gentoo/stage3:latest
# Arch based
- archlinux:latest
- cachyos/cachyos:latest
- manjarolinux/base:latest
container:
image: ${{ matrix.distribution }}
options: --privileged
steps:
- name: Install base dependencies
run: |
# Attempt to run the package installation up to 10 times to mitigate transient network issues
for n in $(seq 1 10); do
{
echo "Attempt #$n of 10 to install base dependencies:"
case "${{ matrix.distribution }}" in
*ubuntu*|*debian*)
apt-get update
apt-get install -y sudo git passwd
;;
*fedora*|*rockylinux*|*almalinux*)
dnf install -y sudo git passwd findutils # findutils=xargs
;;
*suse*)
zypper --non-interactive refresh
zypper --non-interactive install sudo git shadow findutils # findutils=xargs
;;
*gentoo*)
emerge-webrsync
emerge --noreplace --ask=n sudo dev-vcs/git shadow findutils # findutils=xargs
;;
*archlinux*|*cachyos*|*manjaro*)
pacman -Syu --noconfirm
pacman -S --noconfirm sudo git
;;
esac
} && break || sleep 10
done
# Fix PAM configuration for sudo in containers
# Fix /etc/shadow permissions - common issue in container environments
chmod 640 /etc/shadow || chmod 400 /etc/shadow || true
# Disable problematic PAM modules that commonly fail in RHEL-like containers
sed -i 's/^session.*pam_systemd.so/#&/' /etc/pam.d/sudo || true
sed -i 's/^session.*pam_loginuid.so/#&/' /etc/pam.d/sudo || true
# Ensure proper sudoers configuration
echo 'Defaults !requiretty' >> /etc/sudoers
echo 'Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"' >> /etc/sudoers
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
submodules: recursive
path: qmk_firmware
- name: Create test user
run: |
# Create a test user for the bootstrap script
useradd -m -s /bin/bash -U testuser
echo 'testuser:testpassword' | chpasswd || true
# Configure passwordless sudo
echo "root ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers # some distros complain about root not being in sudoers
echo "testuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
# Test sudo functionality
sudo -u testuser whoami || echo "Sudo test failed, but continuing..."
- name: Move QMK repository to test user home
run: |
# Add upstream remote to the cloned repository so `qmk doctor` doesn't flag a warning
git -C qmk_firmware remote add upstream https://github.com/qmk/qmk_firmware.git
# Move the QMK repository to the test user's home directory
mv qmk_firmware /home/testuser/qmk_firmware
chown -R testuser:testuser /home/testuser/qmk_firmware
- name: Run bootstrap script
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Ensure the bootstrap script can access sudo
sudo -u testuser --preserve-env=GITHUB_TOKEN bash -c "
export CONFIRM=1
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
cd /home/testuser
bash /home/testuser/qmk_firmware/util/env-bootstrap.sh
"
- name: Test QMK CLI
run: |
sudo -u testuser bash -c "
export PATH=/home/testuser/.local/bin:\$PATH
cd /home/testuser
qmk setup -y -H /home/testuser/qmk_firmware # setup implies doctor, no need to run it separately
cd /home/testuser/qmk_firmware
qmk mass-compile -j $(nproc) -e DUMP_CI_METADATA=yes -f 'keyboard_name==*onekey*' -km reset -p || touch .failed # Compile a bunch of different platforms
"
cd /home/testuser/qmk_firmware
./util/ci/generate_failure_markdown.sh > $GITHUB_STEP_SUMMARY || true
[ ! -e .failed ] || exit 1
bootstrap-test-macos:
name: Bootstrap (macOS)
strategy:
fail-fast: false
matrix:
os:
- macos-13 # Intel x64
- macos-14 # Apple Silicon ARM64
- macos-15 # Apple Silicon ARM64
- macos-15-intel # Intel x64
- macos-26 # Apple Silicon ARM64
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
submodules: recursive
- name: Run bootstrap script
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Add upstream remote to the cloned repository so `qmk doctor` doesn't flag a warning
git remote add upstream https://github.com/qmk/qmk_firmware.git
# Run the bootstrap script
export CONFIRM=1
sh ./util/env-bootstrap.sh
- name: Test QMK CLI
run: |
# Add QMK CLI to PATH (bootstrap script installs it to ~/.local/bin on macOS)
export PATH="$HOME/.local/bin:$PATH"
qmk setup -y -H . # setup implies doctor, no need to run it separately
qmk mass-compile -j $(sysctl -n hw.ncpu) -e DUMP_CI_METADATA=yes -f 'keyboard_name==*onekey*' -km reset || touch .failed # Compile a bunch of different platforms
./util/ci/generate_failure_markdown.sh > $GITHUB_STEP_SUMMARY || true
[ ! -e .failed ] || exit 1
bootstrap-test-windows:
name: Bootstrap (Windows)
strategy:
fail-fast: false
matrix:
msys-variant:
- mingw64
- clang64
- ucrt64
runs-on: windows-latest
defaults:
run:
shell: msys2 {0}
steps:
- name: Install MSYS2
uses: msys2/setup-msys2@v2
with:
msystem: ${{ matrix.msys-variant }}
pacboy: >-
git:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
submodules: recursive
- name: Run bootstrap script
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Add upstream remote to the cloned repository so `qmk doctor` doesn't flag a warning
git remote add upstream https://github.com/qmk/qmk_firmware.git
# Run the bootstrap script
export CONFIRM=1
sh ./util/env-bootstrap.sh
- name: Test QMK CLI
run: |
# Add QMK CLI to PATH (bootstrap script installs it to /opt/uv/tools/bin on Windows MSYS2)
export PATH="/opt/uv/tools/bin:$PATH"
qmk setup -y -H . # setup implies doctor, no need to run it separately
qmk mass-compile -j $(nproc) -e DUMP_CI_METADATA=yes -f 'keyboard_name==*onekey*' -km reset || touch .failed # Compile a bunch of different platforms
./util/ci/generate_failure_markdown.sh > $GITHUB_STEP_SUMMARY || true
[ ! -e .failed ] || exit 1

View file

@ -4,7 +4,7 @@ QMK presents itself to the host as a regular HID keyboard device, and as such re
There are two notable exceptions: the Caterina bootloader, usually seen on Pro Micros, and the HalfKay bootloader shipped with PJRC Teensys, appear as a serial port and a generic HID device respectively, and so do not require a driver.
We recommend the use of the [Zadig](https://zadig.akeo.ie/) utility. If you have set up the development environment with MSYS2, the `qmk_install.sh` script will have already installed the drivers for you.
We recommend the use of the [Zadig](https://zadig.akeo.ie/) utility. If you have set up the development environment with MSYS2, the QMK CLI installation script will have already installed the drivers for you.
## Installation

View file

@ -44,7 +44,7 @@ Pro Micro (Atmega32u4), make sure to include `CONFIG_USB_ACM=y`. Other devices m
Issues encountered when flashing keyboards on Windows are most often due to having the wrong drivers installed for the bootloader, or none at all.
Re-running the QMK installation script (`./util/qmk_install.sh` from the `qmk_firmware` directory in MSYS2 or WSL) or reinstalling the QMK Toolbox may fix the issue. Alternatively, you can download and run the [`qmk_driver_installer`](https://github.com/qmk/qmk_driver_installer) package manually.
Re-running the QMK installation script (`curl -fsSL https://install.qmk.fm | sh`) or reinstalling the QMK Toolbox may fix the issue. Alternatively, you can download and run the [`qmk_driver_installer`](https://github.com/qmk/qmk_driver_installer) package manually.
If that doesn't work, then you may need to download and run Zadig. See [Bootloader Driver Installation with Zadig](driver_installation_zadig) for more detailed information.

View file

@ -50,90 +50,64 @@ You will need to install [MSYS2](https://www.msys2.org). Once installed, close a
Install the QMK CLI by running:
```sh
pacman --needed --noconfirm --disable-download-timeout -S git mingw-w64-x86_64-python-qmk
curl -fsSL https://install.qmk.fm | sh
```
::::
==== macOS
QMK maintains a Homebrew tap and formula which will automatically install the CLI and all necessary dependencies.
#### Prerequisites
You will need to install Homebrew. Follow the instructions on https://brew.sh.
::: tip
If you are using an Apple Silicon machine, the installation process will take significantly longer because GitHub actions do not have native runners to build binary packages for the ARM and AVR toolchains.
:::
#### Installation
Install the QMK CLI by running:
```sh
brew install qmk/qmk/qmk
curl -fsSL https://install.qmk.fm | sh
```
==== Linux/WSL
#### Installation
::: info
Many Linux distributions are supported, but not all. Mainstream distributions will have best success -- if possible, choose either Debian or its derivatives (such as Ubuntu, or Mint), CentOS or its derivatives (such as Fedora, or Rocky Linux), and Arch or its derivatives (such as Manjaro, or CachyOS).
:::
Install the QMK CLI by running:
```sh
curl -fsSL https://install.qmk.fm | sh
```
::: tip
**Note for WSL users**: By default, the installation process will clone the QMK repository into your WSL home directory, but if you have cloned manually, ensure that it is located inside the WSL instance instead of the Windows filesystem (ie. not in `/mnt`), as accessing it is currently [extremely slow](https://github.com/microsoft/WSL/issues/4197).
:::
#### Prerequisites
You will need to install Git and Python. It's very likely that you already have both, but if not, one of the following commands should install them:
* Debian / Ubuntu / Devuan: `sudo apt install -y git python3-pip`
* Fedora / Red Hat / CentOS: `sudo yum -y install git python3-pip`
* Arch / Manjaro: `sudo pacman --needed --noconfirm -S git python-pip libffi`
* Void: `sudo xbps-install -y git python3-pip`
* Solus: `sudo eopkg -y install git python3`
* Sabayon: `sudo equo install dev-vcs/git dev-python/pip`
* Gentoo: `sudo emerge dev-vcs/git dev-python/pip`
#### Installation
Install the QMK CLI by running:
```sh
python3 -m pip install --user qmk
```
Alternatively, install the QMK CLI as a [uv](https://docs.astral.sh/uv/) managed tool, kept isolated in a virtual environment (requires uv to be installed):
```sh
uv tool install qmk
```
#### Community Packages
These packages are maintained by community members, so may not be up to date or completely functional. If you encounter problems, please report them to their respective maintainers.
On Arch-based distros you can install the CLI from the official repositories (NOTE: at the time of writing this package marks some dependencies as optional that should not be):
```sh
sudo pacman -S qmk
```
You can also try the `qmk-git` package from AUR:
```sh
yay -S qmk-git
```
::: warning
Any QMK packages provided by your distribution's package manager are almost certainly out of date. It is strongly suggested the installation script above is used instead.
:::
==== FreeBSD
#### Installation
::: warning
FreeBSD support is provided on a best-effort basis by the community instead of the QMK maintainers. It is strongly suggested that you use either Windows, macOS, or a supported distribution of Linux instead.
:::
Install the FreeBSD package for QMK CLI by running:
```sh
pkg install -g "py*-qmk"
```
NOTE: remember to follow the instructions printed at the end of installation (use `pkg info -Dg "py*-qmk"` to show them again).
::: info NOTE
Remember to follow the instructions printed at the end of installation (use `pkg info -Dg "py*-qmk"` to show them again).
:::
:::::

View file

@ -6,7 +6,7 @@ The Dactyl uses the [Teensy Loader](https://www.pjrc.com/teensy/loader.html).
Linux users need to modify udev rules as described on the [Teensy
Linux page]. Some distributions provide a binary, maybe called
`teensy-loader-cli`.
`teensy_loader_cli`.
[Teensy Linux page]: https://www.pjrc.com/teensy/loader_linux.html
@ -26,7 +26,7 @@ To flash the firmware:
- Click the button in the Teensy app to download the firmware.
To flash with ´teensy-loader-cli´:
To flash with ´teensy_loader_cli´:
- Build the firmware as above

View file

@ -3,6 +3,8 @@
We list each subcommand here explicitly because all the reliable ways of searching for modules are slow and delay startup.
"""
import os
import platform
import platformdirs
import shlex
import sys
from importlib.util import find_spec
@ -12,6 +14,28 @@ from subprocess import run
from milc import cli, __VERSION__
from milc.questions import yesno
def _get_default_distrib_path():
if 'windows' in platform.platform().lower():
try:
result = cli.run(['cygpath', '-w', '/opt/qmk'])
if result.returncode == 0:
return result.stdout.strip()
except Exception:
pass
return platformdirs.user_data_dir('qmk')
# Ensure the QMK distribution is on the `$PATH` if present. This must be kept in sync with qmk/qmk_cli.
QMK_DISTRIB_DIR = Path(os.environ.get('QMK_DISTRIB_DIR', _get_default_distrib_path()))
if QMK_DISTRIB_DIR.exists():
os.environ['PATH'] = str(QMK_DISTRIB_DIR / 'bin') + os.pathsep + os.environ['PATH']
# Prepend any user-defined path prefix
if 'QMK_PATH_PREFIX' in os.environ:
os.environ['PATH'] = os.environ['QMK_PATH_PREFIX'] + os.pathsep + os.environ['PATH']
import_names = {
# A mapping of package name to importable name
'pep8-naming': 'pep8ext_naming',

View file

@ -1,7 +1,6 @@
"""Check for specific programs.
"""
from enum import Enum
import re
import shutil
from subprocess import DEVNULL, TimeoutExpired
from tempfile import TemporaryDirectory
@ -9,6 +8,7 @@ from pathlib import Path
from milc import cli
from qmk import submodules
from qmk.commands import find_make
class CheckStatus(Enum):
@ -17,7 +17,13 @@ class CheckStatus(Enum):
ERROR = 3
WHICH_MAKE = Path(find_make()).name
ESSENTIAL_BINARIES = {
WHICH_MAKE: {},
'git': {},
'dos2unix': {},
'diff': {},
'dfu-programmer': {},
'avrdude': {},
'dfu-util': {},
@ -30,14 +36,39 @@ ESSENTIAL_BINARIES = {
}
def _parse_gcc_version(version):
m = re.match(r"(\d+)(?:\.(\d+))?(?:\.(\d+))?", version)
def _check_make_version():
last_line = ESSENTIAL_BINARIES[WHICH_MAKE]['output'].split('\n')[0]
version_number = last_line.split()[2]
cli.log.info('Found %s version %s', WHICH_MAKE, version_number)
return {
'major': int(m.group(1)),
'minor': int(m.group(2)) if m.group(2) else 0,
'patch': int(m.group(3)) if m.group(3) else 0,
}
return CheckStatus.OK
def _check_git_version():
last_line = ESSENTIAL_BINARIES['git']['output'].split('\n')[0]
version_number = last_line.split()[2]
cli.log.info('Found git version %s', version_number)
return CheckStatus.OK
def _check_dos2unix_version():
last_line = ESSENTIAL_BINARIES['dos2unix']['output'].split('\n')[0]
version_number = last_line.split()[1]
cli.log.info('Found dos2unix version %s', version_number)
return CheckStatus.OK
def _check_diff_version():
last_line = ESSENTIAL_BINARIES['diff']['output'].split('\n')[0]
if 'Apple diff' in last_line:
version_number = last_line
else:
version_number = last_line.split()[3]
cli.log.info('Found diff version %s', version_number)
return CheckStatus.OK
def _check_arm_gcc_version():
@ -148,16 +179,24 @@ def check_binaries():
"""Iterates through ESSENTIAL_BINARIES and tests them.
"""
ok = CheckStatus.OK
missing_from_path = []
for binary in sorted(ESSENTIAL_BINARIES):
try:
if not is_executable(binary):
if not is_in_path(binary):
ok = CheckStatus.ERROR
missing_from_path.append(binary)
elif not is_executable(binary):
ok = CheckStatus.ERROR
except TimeoutExpired:
cli.log.debug('Timeout checking %s', binary)
if ok != CheckStatus.ERROR:
ok = CheckStatus.WARNING
if missing_from_path:
location_noun = 'its location' if len(missing_from_path) == 1 else 'their locations'
cli.log.error('{fg_red}' + ', '.join(missing_from_path) + f' may need to be installed, or {location_noun} added to your path.')
return ok
@ -165,6 +204,10 @@ def check_binary_versions():
"""Check the versions of ESSENTIAL_BINARIES
"""
checks = {
WHICH_MAKE: _check_make_version,
'git': _check_git_version,
'dos2unix': _check_dos2unix_version,
'diff': _check_diff_version,
'arm-none-eabi-gcc': _check_arm_gcc_version,
'avr-gcc': _check_avr_gcc_version,
'avrdude': _check_avrdude_version,
@ -196,15 +239,18 @@ def check_submodules():
return CheckStatus.OK
def is_executable(command):
"""Returns True if command exists and can be executed.
def is_in_path(command):
"""Returns True if command is found in the path.
"""
# Make sure the command is in the path.
res = shutil.which(command)
if res is None:
if shutil.which(command) is None:
cli.log.error("{fg_red}Can't find %s in your path.", command)
return False
return True
def is_executable(command):
"""Returns True if command can be executed.
"""
# Make sure the command can be executed
version_arg = ESSENTIAL_BINARIES[command].get('version_arg', '--version')
check = cli.run([command, version_arg], combined_output=True, stdin=DEVNULL, timeout=5)

View file

@ -3,7 +3,6 @@
Check out the user's QMK environment and make sure it's ready to compile.
"""
import platform
from subprocess import DEVNULL
from milc import cli
from milc.questions import yesno
@ -16,6 +15,60 @@ from qmk.commands import in_virtualenv
from qmk.userspace import qmk_userspace_paths, qmk_userspace_validate, UserspaceValidationError
def distrib_tests():
def _load_kvp_file(file):
"""Load a simple key=value file into a dictionary
"""
vars = {}
with open(file, 'r') as f:
for line in f:
if '=' in line:
key, value = line.split('=', 1)
vars[key.strip()] = value.strip()
return vars
def _parse_toolchain_release_file(file):
"""Parse the QMK toolchain release info file
"""
try:
vars = _load_kvp_file(file)
return f'{vars.get("TOOLCHAIN_HOST", "unknown")}:{vars.get("TOOLCHAIN_TARGET", "unknown")}:{vars.get("COMMIT_HASH", "unknown")}'
except Exception as e:
cli.log.warning('Error reading QMK toolchain release info file: %s', e)
return f'Unknown toolchain release info file: {file}'
def _parse_flashutils_release_file(file):
"""Parse the QMK flashutils release info file
"""
try:
vars = _load_kvp_file(file)
return f'{vars.get("FLASHUTILS_HOST", "unknown")}:{vars.get("COMMIT_HASH", "unknown")}'
except Exception as e:
cli.log.warning('Error reading QMK flashutils release info file: %s', e)
return f'Unknown flashutils release info file: {file}'
try:
from qmk.cli import QMK_DISTRIB_DIR
if (QMK_DISTRIB_DIR / 'etc').exists():
cli.log.info('Found QMK tools distribution directory: {fg_cyan}%s', QMK_DISTRIB_DIR)
toolchains = [_parse_toolchain_release_file(file) for file in (QMK_DISTRIB_DIR / 'etc').glob('toolchain_release_*')]
if len(toolchains) > 0:
cli.log.info('Found QMK toolchains: {fg_cyan}%s', ', '.join(toolchains))
else:
cli.log.warning('No QMK toolchains manifest found.')
flashutils = [_parse_flashutils_release_file(file) for file in (QMK_DISTRIB_DIR / 'etc').glob('flashutils_release_*')]
if len(flashutils) > 0:
cli.log.info('Found QMK flashutils: {fg_cyan}%s', ', '.join(flashutils))
else:
cli.log.warning('No QMK flashutils manifest found.')
except ImportError:
cli.log.info('QMK tools distribution not found.')
return CheckStatus.OK
def os_tests():
"""Determine our OS and run platform specific tests
"""
@ -124,10 +177,12 @@ def doctor(cli):
* [ ] Compile a trivial program with each compiler
"""
cli.log.info('QMK Doctor is checking your environment.')
cli.log.info('Python version: %s', platform.python_version())
cli.log.info('CLI version: %s', cli.version)
cli.log.info('QMK home: {fg_cyan}%s', QMK_FIRMWARE)
status = os_status = os_tests()
distrib_tests()
userspace_tests(None)
@ -141,12 +196,6 @@ def doctor(cli):
# Make sure the basic CLI tools we need are available and can be executed.
bin_ok = check_binaries()
if bin_ok == CheckStatus.ERROR:
if yesno('Would you like to install dependencies?', default=True):
cli.run(['util/qmk_install.sh', '-y'], stdin=DEVNULL, capture_output=False)
bin_ok = check_binaries()
if bin_ok == CheckStatus.OK:
cli.log.info('All dependencies are installed.')
elif bin_ok == CheckStatus.WARNING:
@ -163,7 +212,6 @@ def doctor(cli):
# Check out the QMK submodules
sub_ok = check_submodules()
if sub_ok == CheckStatus.OK:
cli.log.info('Submodules are up to date.')
else:
@ -186,6 +234,7 @@ def doctor(cli):
cli.log.info('{fg_yellow}QMK is ready to go, but minor problems were found')
return 1
else:
cli.log.info('{fg_red}Major problems detected, please fix these problems before proceeding.')
cli.log.info('{fg_blue}Check out the FAQ (https://docs.qmk.fm/#/faq_build) or join the QMK Discord (https://discord.gg/qmk) for help.')
cli.log.info('{fg_red}Major problems detected, please fix these problems before proceeding.{fg_reset}')
cli.log.info('{fg_blue}If you\'re missing dependencies, try following the instructions on: https://docs.qmk.fm/newbs_getting_started{fg_reset}')
cli.log.info('{fg_blue}Additionally, check out the FAQ (https://docs.qmk.fm/#/faq_build) or join the QMK Discord (https://discord.gg/qmk) for help.{fg_reset}')
return 2

View file

@ -155,10 +155,10 @@ def _flash_atmel_dfu(mcu, file):
def _flash_hid_bootloader(mcu, details, file):
cmd = None
if details == 'halfkay':
if shutil.which('teensy-loader-cli'):
cmd = 'teensy-loader-cli'
elif shutil.which('teensy_loader_cli'):
if shutil.which('teensy_loader_cli'):
cmd = 'teensy_loader_cli'
elif shutil.which('teensy-loader-cli'):
cmd = 'teensy-loader-cli'
# Use 'hid_bootloader_cli' for QMK HID and as a fallback for HalfKay
if not cmd:

View file

@ -14,7 +14,7 @@ from qmk.json_schema import deep_update, json_load, validate
from qmk.keyboard import config_h, rules_mk
from qmk.commands import parse_configurator_json
from qmk.makefile import parse_rules_mk_file
from qmk.math import compute
from qmk.math_ops import compute
from qmk.util import maybe_exit, truthy
true_values = ['1', 'on', 'yes']

View file

@ -175,8 +175,9 @@ def keyboard_completer(prefix, action, parser, parsed_args):
return list_keyboards()
@lru_cache(maxsize=None)
def list_keyboards():
"""Returns a list of all keyboards
"""Returns a list of all keyboards.
"""
# We avoid pathlib here because this is performance critical code.
kb_wildcard = os.path.join(base_path, "**", 'keyboard.json')
@ -184,6 +185,9 @@ def list_keyboards():
found = map(_find_name, paths)
# Convert to posix paths for consistency
found = map(lambda x: str(Path(x).as_posix()), found)
return sorted(set(found))

View file

@ -23,8 +23,8 @@ def compute(expr):
def _eval(node):
if isinstance(node, ast.Num): # <number>
return node.n
if isinstance(node, ast.Constant): # <number>
return node.value
elif isinstance(node, ast.BinOp): # <left> <operator> <right>
return operators[type(node.op)](_eval(node.left), _eval(node.right))
elif isinstance(node, ast.UnaryOp): # <operator> <operand> e.g., -1

View file

@ -5,10 +5,10 @@
# Autodetect teensy loader
ifndef TEENSY_LOADER_CLI
ifneq (, $(shell which teensy-loader-cli 2>/dev/null))
TEENSY_LOADER_CLI ?= teensy-loader-cli
else
ifneq (, $(shell which teensy_loader_cli 2>/dev/null))
TEENSY_LOADER_CLI ?= teensy_loader_cli
else
TEENSY_LOADER_CLI ?= teensy-loader-cli
endif
endif

View file

@ -77,10 +77,10 @@ st-flash: $(BUILD_DIR)/$(TARGET).hex sizeafter
# Autodetect teensy loader
ifndef TEENSY_LOADER_CLI
ifneq (, $(shell which teensy-loader-cli 2>/dev/null))
TEENSY_LOADER_CLI ?= teensy-loader-cli
else
ifneq (, $(shell which teensy_loader_cli 2>/dev/null))
TEENSY_LOADER_CLI ?= teensy_loader_cli
else
TEENSY_LOADER_CLI ?= teensy-loader-cli
endif
endif

594
util/env-bootstrap.sh Executable file
View file

@ -0,0 +1,594 @@
#!/usr/bin/env sh
# Copyright 2025 Nick Brassel (@tzarc)
# SPDX-License-Identifier: GPL-2.0-or-later
################################################################################
# This script will install the QMK CLI, toolchains, and flashing utilities.
################################################################################
# Environment variables:
# CONFIRM: Skip the pre-install delay. (or: --confirm)
# QMK_DISTRIB_DIR: The directory to install the QMK distribution to. (or: --qmk-distrib-dir=...)
# UV_INSTALL_DIR: The directory to install `uv` to. (or: --uv-install-dir=...)
# UV_TOOL_DIR: The directory to install `uv` tools to. (or: --uv-tool-dir=...)
# SKIP_CLEAN: Skip cleaning the distribution directory. (or: --skip-clean)
# SKIP_PACKAGE_MANAGER: Skip installing the necessary packages for the package manager. (or: --skip-package-manager)
# SKIP_UV: Skip installing `uv`. (or: --skip-uv)
# SKIP_QMK_CLI: Skip installing the QMK CLI. (or: --skip-qmk-cli)
# SKIP_QMK_TOOLCHAINS: Skip installing the QMK toolchains. (or: --skip-qmk-toolchains)
# SKIP_QMK_FLASHUTILS: Skip installing the QMK flashing utilities. (or: --skip-qmk-flashutils)
# SKIP_UDEV_RULES: Skip installing the udev rules for Linux. (or: --skip-udev-rules)
# SKIP_WINDOWS_DRIVERS: Skip installing the Windows drivers for the flashing utilities. (or: --skip-windows-drivers)
#
# Arguments above may be negated by prefixing with `--no-` instead (e.g. `--no-skip-clean`).
################################################################################
# Usage:
# curl -fsSL https://raw.githubusercontent.com/qmk/qmk_firmware/master/util/env-bootstrap.sh | sh
#
# Help:
# curl -fsSL https://raw.githubusercontent.com/qmk/qmk_firmware/master/util/env-bootstrap.sh | sh -s -- --help
#
# An example which skips installing `uv` using environment variables:
# curl -fsSL https://raw.githubusercontent.com/qmk/qmk_firmware/master/util/env-bootstrap.sh | SKIP_UV=1 sh
#
# ...or by using command line arguments:
# curl -fsSL https://raw.githubusercontent.com/qmk/qmk_firmware/master/util/env-bootstrap.sh | sh -s -- --skip-uv
#
# Any other configurable items listed above may be specified in the same way.
################################################################################
{ # this ensures the entire script is downloaded #
set -eu
BOOTSTRAP_TMPDIR="$(mktemp -d /tmp/qmk-bootstrap-failure.XXXXXX)"
trap 'rm -rf "$BOOTSTRAP_TMPDIR" >/dev/null 2>&1 || true' EXIT
FAILURE_FILE="${BOOTSTRAP_TMPDIR}/fail"
# Work out which `sed` to use
command -v gsed >/dev/null 2>&1 && SED=gsed || SED=sed
script_args() {
cat <<__EOT__
--help -- Shows this help text
--confirm -- Skips the delay before installation
--uv-install-dir={path} -- The directory to install \`uv\` into
--uv-tool-dir={path} -- The directory to install \`uv\` tools into
--qmk-distrib-dir={path} -- The directory to install the QMK distribution into
--skip-clean -- Skip cleaning the QMK distribution directory
--skip-package-manager -- Skip installing the necessary packages for the package manager
--skip-uv -- Skip installing \`uv\`
--skip-qmk-cli -- Skip installing the QMK CLI
--skip-qmk-toolchains -- Skip installing the QMK toolchains
--skip-qmk-flashutils -- Skip installing the QMK flashing utilities
--skip-udev-rules -- Skip installing the udev rules for Linux
--skip-windows-drivers -- Skip installing the Windows drivers for the flashing utilities
__EOT__
# Hidden:
# --wsl-install -- Installs the WSL variant of qmk_flashutils
}
signal_execution_failure() {
touch "$FAILURE_FILE" >/dev/null 2>&1 || true
}
exit_if_execution_failed() {
if [ -e "$FAILURE_FILE" ]; then
exit 1
fi
}
script_help() {
echo "$(basename ${this_script:-qmk-install.sh}) $(script_args | sort | ${SED} -e 's@^\s*@@g' -e 's@\s\+--.*@@g' -e 's@^@[@' -e 's@$@]@' | tr '\n' ' ')"
echo
echo "Arguments:"
script_args
echo
echo "Switch arguments may be negated by prefixing with '--no-' (e.g. '--no-skip-clean')."
}
script_parse_args() {
local N
local V
while [ ! -z "${1:-}" ]; do
case "$1" in
--help)
script_help
exit 0
;;
--*=*)
N=${1%%=*}
N=${N##--}
N=$(echo $N | tr '-' '_' | tr 'a-z' 'A-Z')
V=${1##*=}
export $N="$V"
;;
--no-*)
N=${1##--no-}
N=$(echo $N | tr '-' '_' | tr 'a-z' 'A-Z')
unset $N
;;
--*)
N=${1##--}
N=$(echo $N | tr '-' '_' | tr 'a-z' 'A-Z')
export $N=true
;;
*)
echo "Unknown argument: '$1'" >&2
echo
script_help >&2
exit 1
;;
esac
shift
unset N
unset V
done
}
nsudo() {
if [ "$(fn_os)" = "windows" ]; then
# No need for sudo under QMK MSYS
return
elif [ $(id -u) -ne 0 ]; then
if [ -n "$(command -v sudo 2>/dev/null || true)" ]; then
echo "sudo"
elif [ -n "$(command -v doas 2>/dev/null || true)" ]; then
echo "doas"
else
echo "Please install 'sudo' or 'doas' to continue." >&2
exit 1
fi
fi
true
}
download_url() {
local url=$1
local filename=${2:-$(basename "$url")}
local quiet=''
if [ -n "$(command -v curl 2>/dev/null || true)" ]; then
[ "$filename" = "-" ] && quiet='-s' || echo "Downloading '$url' => '$filename'" >&2
curl -LSf $quiet -o "$filename" "$url"
elif [ -n "$(command -v wget 2>/dev/null || true)" ]; then
[ "$filename" = "-" ] && quiet='-q' || echo "Downloading '$url' => '$filename'" >&2
wget $quiet "-O$filename" "$url"
else
echo "Please install 'curl' to continue." >&2
exit 1
fi
}
github_api_call() {
local url="$1"
local token="${GITHUB_TOKEN:-${GH_TOKEN:-}}"
if [ -n "${token:-}" ]; then
if [ -n "$(command -v curl 2>/dev/null || true)" ]; then
curl -fsSL -H "Authorization: token $token" -H "Accept: application/vnd.github.v3+json" "https://api.github.com/$url"
elif [ -n "$(command -v wget 2>/dev/null || true)" ]; then
wget -q --header="Authorization: token $token" --header="Accept: application/vnd.github.v3+json" "https://api.github.com/$url" -O -
fi
else
download_url "https://api.github.com/$url" -
fi
}
fn_os() {
local os_name=$(echo ${1:-} | tr 'A-Z' 'a-z')
if [ -z "$os_name" ]; then
os_name=$(uname -s | tr 'A-Z' 'a-z')
fi
case "$os_name" in
*darwin* | *macos* | *apple*)
echo macos
;;
*windows* | *mingw* | *msys*)
echo windows
;;
*linux*)
echo linux
;;
*)
echo unknown
;;
esac
}
fn_arch() {
local arch_name=$(echo ${1:-} | tr 'A-Z' 'a-z')
if [ -z "$arch_name" ]; then
arch_name=$(uname -m | tr 'A-Z' 'a-z')
fi
case "$arch_name" in
*arm64* | *aarch64*)
echo ARM64
;;
*riscv64*)
echo RV64
;;
*x86_64* | *x64*)
echo X64
;;
*)
echo unknown
;;
esac
}
preinstall_delay() {
[ -z "${CONFIRM:-}" ] || return 0
echo >&2
echo "Waiting 10 seconds before proceeding. Press Ctrl+C to cancel installation." >&2
sleep 10
}
get_package_manager_deps() {
case $(fn_os) in
macos) echo "zstd clang-format make hidapi libusb dos2unix git" ;;
windows) echo "base-devel: zstd:p toolchain:p clang:p hidapi:p dos2unix: git: unzip:" ;;
linux)
case $(grep ID /etc/os-release) in
*arch* | *manjaro* | *cachyos*) echo "zstd base-devel clang diffutils wget unzip zip hidapi dos2unix git" ;;
*debian* | *ubuntu*) echo "zstd build-essential clang-format diffutils wget unzip zip libhidapi-hidraw0 dos2unix git" ;;
*fedora*) echo "zstd clang diffutils which gcc git wget unzip zip hidapi dos2unix libusb-devel libusb1-devel libusb-compat-0.1-devel libusb0-devel git epel-release" ;;
*suse*) echo "zstd clang diffutils wget unzip zip libhidapi-hidraw0 dos2unix git libusb-1_0-devel gzip which" ;;
*gentoo*) echo "zstd diffutils wget unzip zip dev-libs/hidapi dos2unix dev-vcs/git dev-libs/libusb app-arch/gzip which" ;;
*)
echo >&2
echo "Sorry, we don't recognize your distribution." >&2
echo >&2
echo "Proceeding with the installation, however you will need to install at least the following tools manually:" >&2
echo " - make, git, curl, zstd, unzip, [lib]hidapi" >&2
echo "Other tools may be required depending on your distribution." >&2
echo >&2
echo "Alternatively, if you prefer Docker, try using the docker image instead:" >&2
echo " - https://docs.qmk.fm/#/getting_started_docker" >&2
;;
esac
;;
*)
# We can only really support macOS, Windows, and Linux at this time due to `uv` requirements.
echo >&2
echo "Sorry, we don't recognize your OS. Try using a compatible OS instead:" >&2
echo " - https://docs.qmk.fm/newbs_getting_started#set-up-your-environment" >&2
echo >&2
echo "If you cannot use a compatible OS, you can try installing the \`qmk\` Python package manually using \`pip\`, most likely requiring a virtual environment:" >&2
echo " % python3 -m pip install qmk" >&2
echo >&2
echo "All other dependencies will need to be installed manually, such as make, git, AVR and ARM toolchains, and associated flashing utilities." >&2
echo >&2
echo "**NOTE**: QMK does not provide official support for your environment. Here be dragons, you are on your own." >&2
signal_execution_failure
;;
esac
}
print_package_manager_deps_and_delay() {
get_package_manager_deps | tr ' ' '\n' | sort | xargs -I'{}' echo " - {}" >&2
exit_if_execution_failed
preinstall_delay || exit 1
}
install_package_manager_deps() {
# Install the necessary packages for the package manager
case $(fn_os) in
macos)
if [ -n "$(command -v brew 2>/dev/null || true)" ]; then
echo "It will also install the following system packages using 'brew':" >&2
print_package_manager_deps_and_delay
brew update
local existing=""
local new=""
for dep in $(get_package_manager_deps); do
if brew list --formula | grep -q "^${dep}\$"; then
existing="${existing:-} $dep"
else
new="${new:-} $dep"
fi
done
if [ -n "${existing:-}" ]; then
brew upgrade $existing
fi
if [ -n "${new:-}" ]; then
brew install $new
fi
else
echo "Please install 'brew' to continue. See https://brew.sh/ for more information." >&2
exit 1
fi
;;
windows)
echo "It will also install the following packages using 'pacman'/'pacboy':" >&2
print_package_manager_deps_and_delay
$(nsudo) pacman --needed --noconfirm --disable-download-timeout -S pactoys
$(nsudo) pacboy sync --needed --noconfirm --disable-download-timeout $(get_package_manager_deps)
;;
linux)
case $(grep ID /etc/os-release) in
*arch* | *manjaro* | *cachyos*)
echo "It will also install the following system packages using 'pacman':" >&2
print_package_manager_deps_and_delay
$(nsudo) pacman --needed --noconfirm -S $(get_package_manager_deps)
;;
*debian* | *ubuntu*)
echo "It will also install the following system packages using 'apt':" >&2
print_package_manager_deps_and_delay
$(nsudo) apt-get update
DEBIAN_FRONTEND=noninteractive \
$(nsudo) apt-get --quiet --yes install $(get_package_manager_deps)
;;
*fedora*)
echo "It will also install the following system packages using 'dnf':" >&2
print_package_manager_deps_and_delay
# Some RHEL-likes need EPEL for hidapi
$(nsudo) dnf -y install epel-release 2>/dev/null || true
# RHEL-likes have some naming differences in libusb packages, so manually handle those
$(nsudo) dnf -y install $(get_package_manager_deps | tr ' ' '\n' | grep -v 'epel-release' | grep -v libusb | tr '\n' ' ')
for pkg in $(get_package_manager_deps | tr ' ' '\n' | grep libusb); do
$(nsudo) dnf -y install "$pkg" 2>/dev/null || true
done
;;
*opensuse* | *suse*)
echo "It will also install development tools as well as the following system packages using 'zypper':" >&2
print_package_manager_deps_and_delay
$(nsudo) zypper --non-interactive refresh
$(nsudo) zypper --non-interactive install -t pattern devel_basis devel_C_C++
$(nsudo) zypper --non-interactive install $(get_package_manager_deps)
;;
*gentoo*)
echo "It will also install the following system packages using 'emerge':" >&2
print_package_manager_deps_and_delay
$(nsudo) emerge --sync
$(nsudo) emerge --noreplace --ask=n $(get_package_manager_deps | tr ' ' '\n') || signal_execution_failure
exit_if_execution_failed
;;
*)
print_package_manager_deps_and_delay
echo "Proceeding with the installation, you will need to ensure prerequisites are installed." >&2
;;
esac
;;
*)
print_package_manager_deps_and_delay
;;
esac
}
install_uv() {
# Install `uv` (or update as necessary)
download_url https://astral.sh/uv/install.sh - | TMPDIR="$(windows_ish_path "${TMPDIR:-}")" UV_INSTALL_DIR="$(windows_ish_path "${UV_INSTALL_DIR:-}")" sh
}
setup_paths() {
# Set up the paths for any of the locations `uv` expects
if [ -n "${XDG_BIN_HOME:-}" ]; then
export PATH="$XDG_BIN_HOME:$PATH"
fi
if [ -n "${XDG_DATA_HOME:-}" ]; then
export PATH="$XDG_DATA_HOME/../bin:$PATH"
fi
[ ! -d "$HOME/.local/bin" ] || export PATH="$HOME/.local/bin:$PATH"
if [ -n "${UV_INSTALL_DIR:-}" ]; then
export PATH="$UV_INSTALL_DIR/bin:$UV_INSTALL_DIR:$PATH" # cater for both "flat" and "hierarchical" installs of `uv`
fi
if [ -n "${UV_TOOL_BIN_DIR:-}" ]; then
export PATH="$UV_TOOL_BIN_DIR:$PATH"
fi
}
uv_command() {
if [ "$(fn_os)" = "windows" ]; then
UV_TOOL_DIR="$(windows_ish_path "${UV_TOOL_DIR:-}")" \
UV_TOOL_BIN_DIR="$(windows_ish_path "${UV_TOOL_BIN_DIR:-}")" \
uv "$@"
else
uv "$@"
fi
}
install_qmk_cli() {
# Install the QMK CLI
uv_command tool install --force --with pip --upgrade --python $PYTHON_TARGET_VERSION qmk
# QMK is installed to...
local qmk_tooldir="$(posix_ish_path "$(uv_command tool dir)/qmk")"
# Activate the environment
if [ -e "$qmk_tooldir/bin" ]; then
. "$qmk_tooldir/bin/activate"
elif [ -e "$qmk_tooldir/Scripts" ]; then
. "$qmk_tooldir/Scripts/activate"
else
echo "Could not find the QMK environment to activate." >&2
exit 1
fi
# Install the QMK dependencies
uv_command pip install --upgrade -r https://raw.githubusercontent.com/qmk/qmk_firmware/refs/heads/master/requirements.txt
uv_command pip install --upgrade -r https://raw.githubusercontent.com/qmk/qmk_firmware/refs/heads/master/requirements-dev.txt
# Deactivate the environment
deactivate
}
install_toolchains() {
# Get the latest toolchain release from https://github.com/qmk/qmk_toolchains
local latest_toolchains_release=$(github_api_call repos/qmk/qmk_toolchains/releases/latest - | grep -oE '"tag_name": "[^"]+' | grep -oE '[^"]+$')
# Download the specific release asset with a matching keyword
local toolchain_url=$(github_api_call repos/qmk/qmk_toolchains/releases/tags/$latest_toolchains_release - | grep -oE '"browser_download_url": "[^"]+"' | grep -oE 'https://[^"]+' | grep $(fn_os)$(fn_arch))
if [ -z "$toolchain_url" ]; then
echo "No toolchain found for this OS/Arch combination." >&2
exit 1
fi
# Download the toolchain release to the toolchains location
echo "Downloading compiler toolchains..." >&2
local target_file="$QMK_DISTRIB_DIR/$(basename "$toolchain_url")"
download_url "$toolchain_url" "$target_file"
# Extract the toolchain
echo "Extracting compiler toolchains to '$QMK_DISTRIB_DIR'..." >&2
zstdcat "$target_file" | tar xf - -C "$QMK_DISTRIB_DIR" --strip-components=1
}
install_flashing_tools() {
local osarchvariant="$(fn_os)$(fn_arch)"
# Special case for WSL
if [ -n "${WSL_INSTALL:-}" ] || [ -n "${WSL_DISTRO_NAME:-}" ] || [ -f /proc/sys/fs/binfmt_misc/WSLInterop ]; then
osarchvariant="windowsWSL"
fi
# Get the latest flashing tools release from https://github.com/qmk/qmk_flashutils
local latest_flashutils_release=$(github_api_call repos/qmk/qmk_flashutils/releases/latest - | grep -oE '"tag_name": "[^"]+' | grep -oE '[^"]+$')
# Download the specific release asset with a matching keyword
local flashutils_url=$(github_api_call repos/qmk/qmk_flashutils/releases/tags/$latest_flashutils_release - | grep -oE '"browser_download_url": "[^"]+"' | grep -oE 'https://[^"]+' | grep "$osarchvariant")
if [ -z "$flashutils_url" ]; then
echo "No flashing tools found for this OS/Arch combination." >&2
exit 1
fi
# Download the flashing tools release to the toolchains location
echo "Downloading flashing tools..." >&2
local target_file="$QMK_DISTRIB_DIR/$(basename "$flashutils_url")"
download_url "$flashutils_url" "$target_file"
# Extract the flashing tools
echo "Extracting flashing tools to '$QMK_DISTRIB_DIR'..." >&2
zstdcat "$target_file" | tar xf - -C "$QMK_DISTRIB_DIR/bin"
# Move the release file to etc
mv "$QMK_DISTRIB_DIR/bin/flashutils_release"* "$QMK_DISTRIB_DIR/etc"
}
install_linux_udev_rules() {
# Download the udev rules to the toolchains location
echo "Downloading QMK udev rules file..." >&2
local qmk_rules_target_file="$QMK_DISTRIB_DIR/50-qmk.rules"
download_url "https://raw.githubusercontent.com/qmk/qmk_firmware/refs/heads/master/util/udev/50-qmk.rules" "$qmk_rules_target_file"
# Install the udev rules -- path list is aligned with qmk doctor's linux.py
local udev_rules_paths="
/usr/lib/udev/rules.d
/usr/local/lib/udev/rules.d
/run/udev/rules.d
/etc/udev/rules.d
"
for udev_rules_dir in $udev_rules_paths; do
if [ -d "$udev_rules_dir" ]; then
echo "Installing udev rules to $udev_rules_dir/50-qmk.rules ..." >&2
$(nsudo) mv "$qmk_rules_target_file" "$udev_rules_dir"
$(nsudo) chown 0:0 "$udev_rules_dir/50-qmk.rules"
$(nsudo) chmod 644 "$udev_rules_dir/50-qmk.rules"
break
fi
done
# Reload udev rules
if command -v udevadm >/dev/null 2>&1; then
echo "Reloading udev rules..." >&2
$(nsudo) udevadm control --reload-rules || true
$(nsudo) udevadm trigger || true
else
echo "udevadm not found, skipping udev rules reload." >&2
fi
}
install_windows_drivers() {
# Get the latest driver installer release from https://github.com/qmk/qmk_driver_installer
local latest_driver_installer_release=$(github_api_call repos/qmk/qmk_driver_installer/releases/latest - | grep -oE '"tag_name": "[^"]+' | grep -oE '[^"]+$')
# Download the specific release asset
local driver_installer_url=$(github_api_call repos/qmk/qmk_driver_installer/releases/tags/$latest_driver_installer_release - | grep -oE '"browser_download_url": "[^"]+"' | grep -oE 'https://[^"]+' | grep '\.exe')
if [ -z "$driver_installer_url" ]; then
echo "No driver installer found." >&2
exit 1
fi
# Download the driver installer release to the toolchains location
echo "Downloading driver installer..." >&2
local target_file="$QMK_DISTRIB_DIR/$(basename "$driver_installer_url")"
download_url "$driver_installer_url" "$target_file"
# Download the drivers list
download_url "https://raw.githubusercontent.com/qmk/qmk_firmware/refs/heads/master/util/drivers.txt" "$QMK_DISTRIB_DIR/drivers.txt"
# Execute the driver installer
cd "$QMK_DISTRIB_DIR"
cmd.exe //c "qmk_driver_installer.exe --all --force drivers.txt"
cd -
# Remove the temporary files
rm -f "$QMK_DISTRIB_DIR/qmk_driver_installer.exe" "$QMK_DISTRIB_DIR/drivers.txt" || true
}
clean_tarballs() {
# Clean up the tarballs
rm -f "$QMK_DISTRIB_DIR"/*.tar.zst || true
}
windows_ish_path() {
[ -n "$1" ] || return 0
[ "$(uname -o 2>/dev/null || true)" = "Msys" ] && cygpath -w "$1" || echo "$1"
}
posix_ish_path() {
[ -n "$1" ] || return 0
[ "$(uname -o 2>/dev/null || true)" = "Msys" ] && cygpath -u "$1" || echo "$1"
}
# Set the Python version we want to use with the QMK CLI
export PYTHON_TARGET_VERSION=${PYTHON_TARGET_VERSION:-3.14}
# Windows/MSYS doesn't like `/tmp` so we need to set a different temporary directory.
# Also set the default `UV_INSTALL_DIR` and `QMK_DISTRIB_DIR` to locations which don't pollute the user's home directory, keeping the installation internal to MSYS.
if [ "$(uname -o 2>/dev/null || true)" = "Msys" ]; then
export TMPDIR="$(posix_ish_path "$TMP")"
export UV_INSTALL_DIR="$(posix_ish_path "${UV_INSTALL_DIR:-/opt/uv}")"
export QMK_DISTRIB_DIR="$(posix_ish_path "${QMK_DISTRIB_DIR:-/opt/qmk}")"
export UV_TOOL_DIR="$(posix_ish_path "${UV_TOOL_DIR:-"$UV_INSTALL_DIR/tools"}")"
export UV_TOOL_BIN_DIR="$(posix_ish_path "$UV_TOOL_DIR/bin")"
fi
script_parse_args "$@"
echo "This QMK CLI installation script will install \`uv\`, the QMK CLI, as well as QMK-supplied toolchains and flashing utilities." >&2
[ -z "${SKIP_PACKAGE_MANAGER:-}" ] || { preinstall_delay || exit 1; }
[ -n "${SKIP_PACKAGE_MANAGER:-}" ] || install_package_manager_deps
[ -n "${SKIP_UV:-}" ] || install_uv
# Make sure the usual `uv` and other associated directories are on the $PATH
setup_paths
# Work out where we want to install the distribution and tools now that `uv` is installed
export QMK_DISTRIB_DIR="$(posix_ish_path "${QMK_DISTRIB_DIR:-$(printf 'import platformdirs\nprint(platformdirs.user_data_dir("qmk"))' | uv_command run --quiet --python $PYTHON_TARGET_VERSION --with platformdirs -)}")"
# Clear out the distrib directory if necessary
if [ -z "${SKIP_CLEAN:-}" ] || [ -z "${SKIP_QMK_TOOLCHAINS:-}" -a -z "${SKIP_QMK_FLASHUTILS:-}" ]; then
if [ -d "$QMK_DISTRIB_DIR" ]; then
echo "Removing old QMK distribution..." >&2
rm -rf "$QMK_DISTRIB_DIR"
fi
fi
mkdir -p "$QMK_DISTRIB_DIR"
[ -n "${SKIP_QMK_CLI:-}" ] || install_qmk_cli
[ -n "${SKIP_QMK_TOOLCHAINS:-}" ] || install_toolchains
[ -n "${SKIP_QMK_FLASHUTILS:-}" ] || install_flashing_tools
if [ "$(uname -s 2>/dev/null || true)" = "Linux" ]; then
[ -n "${SKIP_UDEV_RULES:-}" ] || install_linux_udev_rules
fi
if [ "$(uname -o 2>/dev/null || true)" = "Msys" ]; then
[ -n "${SKIP_WINDOWS_DRIVERS:-}" ] || install_windows_drivers
fi
clean_tarballs
# Notify the user that they may need to restart their shell to get the `qmk` command
echo >&2
echo "QMK CLI installation complete." >&2
echo "The QMK CLI has been installed to '$(posix_ish_path "$(dirname "$(command -v qmk)")")'." >&2
echo "The QMK CLI venv has been created at '$(posix_ish_path "$(uv_command tool dir)/qmk")'." >&2
echo "Toolchains and flashing utilities have been installed to '$QMK_DISTRIB_DIR'." >&2
echo >&2
echo "You may need to restart your shell to gain access to the 'qmk' command." >&2
echo "Alternatively, add "$(posix_ish_path "$(dirname "$(command -v qmk)")")" to your \$PATH:" >&2
echo " export PATH=\"$(posix_ish_path "$(dirname "$(command -v qmk)")"):\$PATH\"" >&2
} # this ensures the entire script is downloaded #