summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/bootstrap_testing.yml251
-rw-r--r--docs/driver_installation_zadig.md2
-rw-r--r--docs/faq_build.md2
-rw-r--r--docs/newbs_getting_started.md66
-rw-r--r--keyboards/handwired/dactyl/readme.md4
-rw-r--r--lib/python/qmk/cli/__init__.py24
-rw-r--r--lib/python/qmk/cli/doctor/check.py74
-rwxr-xr-xlib/python/qmk/cli/doctor/main.py69
-rw-r--r--lib/python/qmk/flashers.py6
-rw-r--r--lib/python/qmk/info.py2
-rw-r--r--lib/python/qmk/keyboard.py6
-rw-r--r--lib/python/qmk/math_ops.py (renamed from lib/python/qmk/math.py)4
-rw-r--r--platforms/avr/flash.mk6
-rw-r--r--platforms/chibios/flash.mk6
-rwxr-xr-xutil/env-bootstrap.sh594
15 files changed, 1029 insertions, 87 deletions
diff --git a/.github/workflows/bootstrap_testing.yml b/.github/workflows/bootstrap_testing.yml
new file mode 100644
index 0000000000..997db3f0b2
--- /dev/null
+++ b/.github/workflows/bootstrap_testing.yml
@@ -0,0 +1,251 @@
1name: Bootstrap Script Testing
2
3on:
4 push:
5 branches: [bootstrap]
6 paths:
7 - "util/env-bootstrap.sh"
8 - ".github/workflows/bootstrap_testing.yml"
9 - "lib/python/**"
10 pull_request:
11 branches: [master, develop, xap]
12 paths:
13 - "util/env-bootstrap.sh"
14 - ".github/workflows/bootstrap_testing.yml"
15 - "lib/python/**"
16 workflow_dispatch:
17
18permissions:
19 contents: read
20
21jobs:
22 bootstrap-test-linux:
23 name: Bootstrap (Linux)
24 runs-on: ubuntu-latest
25
26 strategy:
27 fail-fast: false
28 matrix:
29 distribution:
30 # Ubuntu/Debian based
31 - debian:11
32 - debian:12
33 - debian:13
34 - ubuntu:20.04
35 - ubuntu:22.04
36 - ubuntu:24.04
37
38 # RHEL/CentOS/Fedora based
39 - fedora:41
40 - fedora:42
41 - fedora:43
42 - rockylinux:8
43 - rockylinux:9
44 - rockylinux/rockylinux:10
45 - almalinux:8
46 - almalinux:9
47 - almalinux:10
48
49 # OpenSUSE based (we skip Tumbleweed as it has issues with package versions between pattern installs and other dependencies preinstalled into the base container)
50 - opensuse/leap:latest
51
52 # Gentoo-based
53 - gentoo/stage3:latest
54
55 # Arch based
56 - archlinux:latest
57 - cachyos/cachyos:latest
58 - manjarolinux/base:latest
59
60 container:
61 image: ${{ matrix.distribution }}
62 options: --privileged
63
64 steps:
65 - name: Install base dependencies
66 run: |
67 # Attempt to run the package installation up to 10 times to mitigate transient network issues
68 for n in $(seq 1 10); do
69 {
70 echo "Attempt #$n of 10 to install base dependencies:"
71 case "${{ matrix.distribution }}" in
72 *ubuntu*|*debian*)
73 apt-get update
74 apt-get install -y sudo git passwd
75 ;;
76 *fedora*|*rockylinux*|*almalinux*)
77 dnf install -y sudo git passwd findutils # findutils=xargs
78 ;;
79 *suse*)
80 zypper --non-interactive refresh
81 zypper --non-interactive install sudo git shadow findutils # findutils=xargs
82 ;;
83 *gentoo*)
84 emerge-webrsync
85 emerge --noreplace --ask=n sudo dev-vcs/git shadow findutils # findutils=xargs
86 ;;
87 *archlinux*|*cachyos*|*manjaro*)
88 pacman -Syu --noconfirm
89 pacman -S --noconfirm sudo git
90 ;;
91 esac
92 } && break || sleep 10
93 done
94
95 # Fix PAM configuration for sudo in containers
96 # Fix /etc/shadow permissions - common issue in container environments
97 chmod 640 /etc/shadow || chmod 400 /etc/shadow || true
98
99 # Disable problematic PAM modules that commonly fail in RHEL-like containers
100 sed -i 's/^session.*pam_systemd.so/#&/' /etc/pam.d/sudo || true
101 sed -i 's/^session.*pam_loginuid.so/#&/' /etc/pam.d/sudo || true
102
103 # Ensure proper sudoers configuration
104 echo 'Defaults !requiretty' >> /etc/sudoers
105 echo 'Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"' >> /etc/sudoers
106
107 - name: Checkout repository
108 uses: actions/checkout@v4
109 with:
110 fetch-depth: 1
111 submodules: recursive
112 path: qmk_firmware
113
114 - name: Create test user
115 run: |
116 # Create a test user for the bootstrap script
117 useradd -m -s /bin/bash -U testuser
118 echo 'testuser:testpassword' | chpasswd || true
119
120 # Configure passwordless sudo
121 echo "root ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers # some distros complain about root not being in sudoers
122 echo "testuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
123
124 # Test sudo functionality
125 sudo -u testuser whoami || echo "Sudo test failed, but continuing..."
126
127 - name: Move QMK repository to test user home
128 run: |
129 # Add upstream remote to the cloned repository so `qmk doctor` doesn't flag a warning
130 git -C qmk_firmware remote add upstream https://github.com/qmk/qmk_firmware.git
131 # Move the QMK repository to the test user's home directory
132 mv qmk_firmware /home/testuser/qmk_firmware
133 chown -R testuser:testuser /home/testuser/qmk_firmware
134
135 - name: Run bootstrap script
136 env:
137 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
138 run: |
139 # Ensure the bootstrap script can access sudo
140 sudo -u testuser --preserve-env=GITHUB_TOKEN bash -c "
141 export CONFIRM=1
142 export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
143 cd /home/testuser
144 bash /home/testuser/qmk_firmware/util/env-bootstrap.sh
145 "
146
147 - name: Test QMK CLI
148 run: |
149 sudo -u testuser bash -c "
150 export PATH=/home/testuser/.local/bin:\$PATH
151 cd /home/testuser
152 qmk setup -y -H /home/testuser/qmk_firmware # setup implies doctor, no need to run it separately
153 cd /home/testuser/qmk_firmware
154 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
155 "
156
157 cd /home/testuser/qmk_firmware
158 ./util/ci/generate_failure_markdown.sh > $GITHUB_STEP_SUMMARY || true
159 [ ! -e .failed ] || exit 1
160
161 bootstrap-test-macos:
162 name: Bootstrap (macOS)
163 strategy:
164 fail-fast: false
165 matrix:
166 os:
167 - macos-13 # Intel x64
168 - macos-14 # Apple Silicon ARM64
169 - macos-15 # Apple Silicon ARM64
170 - macos-15-intel # Intel x64
171 - macos-26 # Apple Silicon ARM64
172
173 runs-on: ${{ matrix.os }}
174
175 steps:
176 - name: Checkout repository
177 uses: actions/checkout@v4
178 with:
179 fetch-depth: 1
180 submodules: recursive
181
182 - name: Run bootstrap script
183 env:
184 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
185 run: |
186 # Add upstream remote to the cloned repository so `qmk doctor` doesn't flag a warning
187 git remote add upstream https://github.com/qmk/qmk_firmware.git
188 # Run the bootstrap script
189 export CONFIRM=1
190 sh ./util/env-bootstrap.sh
191
192 - name: Test QMK CLI
193 run: |
194 # Add QMK CLI to PATH (bootstrap script installs it to ~/.local/bin on macOS)
195 export PATH="$HOME/.local/bin:$PATH"
196 qmk setup -y -H . # setup implies doctor, no need to run it separately
197 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
198
199 ./util/ci/generate_failure_markdown.sh > $GITHUB_STEP_SUMMARY || true
200 [ ! -e .failed ] || exit 1
201
202 bootstrap-test-windows:
203 name: Bootstrap (Windows)
204
205 strategy:
206 fail-fast: false
207 matrix:
208 msys-variant:
209 - mingw64
210 - clang64
211 - ucrt64
212
213 runs-on: windows-latest
214 defaults:
215 run:
216 shell: msys2 {0}
217
218 steps:
219 - name: Install MSYS2
220 uses: msys2/setup-msys2@v2
221 with:
222 msystem: ${{ matrix.msys-variant }}
223 pacboy: >-
224 git:
225
226 - name: Checkout repository
227 uses: actions/checkout@v4
228 with:
229 fetch-depth: 1
230 submodules: recursive
231
232 - name: Run bootstrap script
233 env:
234 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
235 run: |
236 # Add upstream remote to the cloned repository so `qmk doctor` doesn't flag a warning
237 git remote add upstream https://github.com/qmk/qmk_firmware.git
238 # Run the bootstrap script
239 export CONFIRM=1
240 sh ./util/env-bootstrap.sh
241
242 - name: Test QMK CLI
243 run: |
244 # Add QMK CLI to PATH (bootstrap script installs it to /opt/uv/tools/bin on Windows MSYS2)
245 export PATH="/opt/uv/tools/bin:$PATH"
246 qmk setup -y -H . # setup implies doctor, no need to run it separately
247 qmk mass-compile -j $(nproc) -e DUMP_CI_METADATA=yes -f 'keyboard_name==*onekey*' -km reset || touch .failed # Compile a bunch of different platforms
248
249 ./util/ci/generate_failure_markdown.sh > $GITHUB_STEP_SUMMARY || true
250 [ ! -e .failed ] || exit 1
251
diff --git a/docs/driver_installation_zadig.md b/docs/driver_installation_zadig.md
index 6fbcfa3bff..13b445b9b6 100644
--- a/docs/driver_installation_zadig.md
+++ b/docs/driver_installation_zadig.md
@@ -4,7 +4,7 @@ QMK presents itself to the host as a regular HID keyboard device, and as such re
4 4
5There 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. 5There 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.
6 6
7We 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. 7We 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.
8 8
9## Installation 9## Installation
10 10
diff --git a/docs/faq_build.md b/docs/faq_build.md
index 54ed576c70..05cb3251c8 100644
--- a/docs/faq_build.md
+++ b/docs/faq_build.md
@@ -44,7 +44,7 @@ Pro Micro (Atmega32u4), make sure to include `CONFIG_USB_ACM=y`. Other devices m
44 44
45Issues encountered when flashing keyboards on Windows are most often due to having the wrong drivers installed for the bootloader, or none at all. 45Issues encountered when flashing keyboards on Windows are most often due to having the wrong drivers installed for the bootloader, or none at all.
46 46
47Re-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. 47Re-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.
48 48
49If 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. 49If 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.
50 50
diff --git a/docs/newbs_getting_started.md b/docs/newbs_getting_started.md
index 1d2b60781b..1bc658b9b1 100644
--- a/docs/newbs_getting_started.md
+++ b/docs/newbs_getting_started.md
@@ -50,90 +50,64 @@ You will need to install [MSYS2](https://www.msys2.org). Once installed, close a
50Install the QMK CLI by running: 50Install the QMK CLI by running:
51 51
52```sh 52```sh
53pacman --needed --noconfirm --disable-download-timeout -S git mingw-w64-x86_64-python-qmk 53curl -fsSL https://install.qmk.fm | sh
54``` 54```
55 55
56:::: 56::::
57 57
58==== macOS 58==== macOS
59 59
60QMK maintains a Homebrew tap and formula which will automatically install the CLI and all necessary dependencies.
61
62#### Prerequisites 60#### Prerequisites
63 61
64You will need to install Homebrew. Follow the instructions on https://brew.sh. 62You will need to install Homebrew. Follow the instructions on https://brew.sh.
65 63
66::: tip
67If 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.
68:::
69
70#### Installation 64#### Installation
71 65
72Install the QMK CLI by running: 66Install the QMK CLI by running:
73 67
74```sh 68```sh
75brew install qmk/qmk/qmk 69curl -fsSL https://install.qmk.fm | sh
76``` 70```
77 71
78==== Linux/WSL 72==== Linux/WSL
79 73
80::: tip
81**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).
82:::
83
84#### Prerequisites
85
86You 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:
87
88* Debian / Ubuntu / Devuan: `sudo apt install -y git python3-pip`
89* Fedora / Red Hat / CentOS: `sudo yum -y install git python3-pip`
90* Arch / Manjaro: `sudo pacman --needed --noconfirm -S git python-pip libffi`
91* Void: `sudo xbps-install -y git python3-pip`
92* Solus: `sudo eopkg -y install git python3`
93* Sabayon: `sudo equo install dev-vcs/git dev-python/pip`
94* Gentoo: `sudo emerge dev-vcs/git dev-python/pip`
95
96#### Installation 74#### Installation
97 75
98Install the QMK CLI by running: 76::: info
99 77Many 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).
100```sh 78:::
101python3 -m pip install --user qmk
102```
103
104Alternatively, 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):
105
106```sh
107uv tool install qmk
108```
109
110#### Community Packages
111
112These 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.
113 79
114On 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): 80Install the QMK CLI by running:
115 81
116```sh 82```sh
117sudo pacman -S qmk 83curl -fsSL https://install.qmk.fm | sh
118``` 84```
119 85
120You can also try the `qmk-git` package from AUR: 86::: tip
87**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).
88:::
121 89
122```sh 90::: warning
123yay -S qmk-git 91Any 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.
124``` 92:::
125 93
126==== FreeBSD 94==== FreeBSD
127 95
128#### Installation 96#### Installation
129 97
98::: warning
99FreeBSD 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.
100:::
101
130Install the FreeBSD package for QMK CLI by running: 102Install the FreeBSD package for QMK CLI by running:
131 103
132```sh 104```sh
133pkg install -g "py*-qmk" 105pkg install -g "py*-qmk"
134``` 106```
135 107
136NOTE: remember to follow the instructions printed at the end of installation (use `pkg info -Dg "py*-qmk"` to show them again). 108::: info NOTE
109Remember to follow the instructions printed at the end of installation (use `pkg info -Dg "py*-qmk"` to show them again).
110:::
137 111
138::::: 112:::::
139 113
diff --git a/keyboards/handwired/dactyl/readme.md b/keyboards/handwired/dactyl/readme.md
index e99df7f5a2..608a1b7fe3 100644
--- a/keyboards/handwired/dactyl/readme.md
+++ b/keyboards/handwired/dactyl/readme.md
@@ -6,7 +6,7 @@ The Dactyl uses the [Teensy Loader](https://www.pjrc.com/teensy/loader.html).
6 6
7Linux users need to modify udev rules as described on the [Teensy 7Linux users need to modify udev rules as described on the [Teensy
8Linux page]. Some distributions provide a binary, maybe called 8Linux page]. Some distributions provide a binary, maybe called
9`teensy-loader-cli`. 9`teensy_loader_cli`.
10 10
11[Teensy Linux page]: https://www.pjrc.com/teensy/loader_linux.html 11[Teensy Linux page]: https://www.pjrc.com/teensy/loader_linux.html
12 12
@@ -26,7 +26,7 @@ To flash the firmware:
26 26
27 - Click the button in the Teensy app to download the firmware. 27 - Click the button in the Teensy app to download the firmware.
28 28
29To flash with ´teensy-loader-cli´: 29To flash with ´teensy_loader_cli´:
30 30
31 - Build the firmware as above 31 - Build the firmware as above
32 32
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py
index 26905ec134..dc2e4726a5 100644
--- a/lib/python/qmk/cli/__init__.py
+++ b/lib/python/qmk/cli/__init__.py
@@ -3,6 +3,8 @@
3We list each subcommand here explicitly because all the reliable ways of searching for modules are slow and delay startup. 3We list each subcommand here explicitly because all the reliable ways of searching for modules are slow and delay startup.
4""" 4"""
5import os 5import os
6import platform
7import platformdirs
6import shlex 8import shlex
7import sys 9import sys
8from importlib.util import find_spec 10from importlib.util import find_spec
@@ -12,6 +14,28 @@ from subprocess import run
12from milc import cli, __VERSION__ 14from milc import cli, __VERSION__
13from milc.questions import yesno 15from milc.questions import yesno
14 16
17
18def _get_default_distrib_path():
19 if 'windows' in platform.platform().lower():
20 try:
21 result = cli.run(['cygpath', '-w', '/opt/qmk'])
22 if result.returncode == 0:
23 return result.stdout.strip()
24 except Exception:
25 pass
26
27 return platformdirs.user_data_dir('qmk')
28
29
30# Ensure the QMK distribution is on the `$PATH` if present. This must be kept in sync with qmk/qmk_cli.
31QMK_DISTRIB_DIR = Path(os.environ.get('QMK_DISTRIB_DIR', _get_default_distrib_path()))
32if QMK_DISTRIB_DIR.exists():
33 os.environ['PATH'] = str(QMK_DISTRIB_DIR / 'bin') + os.pathsep + os.environ['PATH']
34
35# Prepend any user-defined path prefix
36if 'QMK_PATH_PREFIX' in os.environ:
37 os.environ['PATH'] = os.environ['QMK_PATH_PREFIX'] + os.pathsep + os.environ['PATH']
38
15import_names = { 39import_names = {
16 # A mapping of package name to importable name 40 # A mapping of package name to importable name
17 'pep8-naming': 'pep8ext_naming', 41 'pep8-naming': 'pep8ext_naming',
diff --git a/lib/python/qmk/cli/doctor/check.py b/lib/python/qmk/cli/doctor/check.py
index 51b0f0c80a..8a13cb0832 100644
--- a/lib/python/qmk/cli/doctor/check.py
+++ b/lib/python/qmk/cli/doctor/check.py
@@ -1,7 +1,6 @@
1"""Check for specific programs. 1"""Check for specific programs.
2""" 2"""
3from enum import Enum 3from enum import Enum
4import re
5import shutil 4import shutil
6from subprocess import DEVNULL, TimeoutExpired 5from subprocess import DEVNULL, TimeoutExpired
7from tempfile import TemporaryDirectory 6from tempfile import TemporaryDirectory
@@ -9,6 +8,7 @@ from pathlib import Path
9 8
10from milc import cli 9from milc import cli
11from qmk import submodules 10from qmk import submodules
11from qmk.commands import find_make
12 12
13 13
14class CheckStatus(Enum): 14class CheckStatus(Enum):
@@ -17,7 +17,13 @@ class CheckStatus(Enum):
17 ERROR = 3 17 ERROR = 3
18 18
19 19
20WHICH_MAKE = Path(find_make()).name
21
20ESSENTIAL_BINARIES = { 22ESSENTIAL_BINARIES = {
23 WHICH_MAKE: {},
24 'git': {},
25 'dos2unix': {},
26 'diff': {},
21 'dfu-programmer': {}, 27 'dfu-programmer': {},
22 'avrdude': {}, 28 'avrdude': {},
23 'dfu-util': {}, 29 'dfu-util': {},
@@ -30,14 +36,39 @@ ESSENTIAL_BINARIES = {
30} 36}
31 37
32 38
33def _parse_gcc_version(version): 39def _check_make_version():
34 m = re.match(r"(\d+)(?:\.(\d+))?(?:\.(\d+))?", version) 40 last_line = ESSENTIAL_BINARIES[WHICH_MAKE]['output'].split('\n')[0]
41 version_number = last_line.split()[2]
42 cli.log.info('Found %s version %s', WHICH_MAKE, version_number)
35 43
36 return { 44 return CheckStatus.OK
37 'major': int(m.group(1)), 45
38 'minor': int(m.group(2)) if m.group(2) else 0, 46
39 'patch': int(m.group(3)) if m.group(3) else 0, 47def _check_git_version():
40 } 48 last_line = ESSENTIAL_BINARIES['git']['output'].split('\n')[0]
49 version_number = last_line.split()[2]
50 cli.log.info('Found git version %s', version_number)
51
52 return CheckStatus.OK
53
54
55def _check_dos2unix_version():
56 last_line = ESSENTIAL_BINARIES['dos2unix']['output'].split('\n')[0]
57 version_number = last_line.split()[1]
58 cli.log.info('Found dos2unix version %s', version_number)
59
60 return CheckStatus.OK
61
62
63def _check_diff_version():
64 last_line = ESSENTIAL_BINARIES['diff']['output'].split('\n')[0]
65 if 'Apple diff' in last_line:
66 version_number = last_line
67 else:
68 version_number = last_line.split()[3]
69 cli.log.info('Found diff version %s', version_number)
70
71 return CheckStatus.OK
41 72
42 73
43def _check_arm_gcc_version(): 74def _check_arm_gcc_version():
@@ -148,16 +179,24 @@ def check_binaries():
148 """Iterates through ESSENTIAL_BINARIES and tests them. 179 """Iterates through ESSENTIAL_BINARIES and tests them.
149 """ 180 """
150 ok = CheckStatus.OK 181 ok = CheckStatus.OK
182 missing_from_path = []
151 183
152 for binary in sorted(ESSENTIAL_BINARIES): 184 for binary in sorted(ESSENTIAL_BINARIES):
153 try: 185 try:
154 if not is_executable(binary): 186 if not is_in_path(binary):
187 ok = CheckStatus.ERROR
188 missing_from_path.append(binary)
189 elif not is_executable(binary):
155 ok = CheckStatus.ERROR 190 ok = CheckStatus.ERROR
156 except TimeoutExpired: 191 except TimeoutExpired:
157 cli.log.debug('Timeout checking %s', binary) 192 cli.log.debug('Timeout checking %s', binary)
158 if ok != CheckStatus.ERROR: 193 if ok != CheckStatus.ERROR:
159 ok = CheckStatus.WARNING 194 ok = CheckStatus.WARNING
160 195
196 if missing_from_path:
197 location_noun = 'its location' if len(missing_from_path) == 1 else 'their locations'
198 cli.log.error('{fg_red}' + ', '.join(missing_from_path) + f' may need to be installed, or {location_noun} added to your path.')
199
161 return ok 200 return ok
162 201
163 202
@@ -165,6 +204,10 @@ def check_binary_versions():
165 """Check the versions of ESSENTIAL_BINARIES 204 """Check the versions of ESSENTIAL_BINARIES
166 """ 205 """
167 checks = { 206 checks = {
207 WHICH_MAKE: _check_make_version,
208 'git': _check_git_version,
209 'dos2unix': _check_dos2unix_version,
210 'diff': _check_diff_version,
168 'arm-none-eabi-gcc': _check_arm_gcc_version, 211 'arm-none-eabi-gcc': _check_arm_gcc_version,
169 'avr-gcc': _check_avr_gcc_version, 212 'avr-gcc': _check_avr_gcc_version,
170 'avrdude': _check_avrdude_version, 213 'avrdude': _check_avrdude_version,
@@ -196,15 +239,18 @@ def check_submodules():
196 return CheckStatus.OK 239 return CheckStatus.OK
197 240
198 241
199def is_executable(command): 242def is_in_path(command):
200 """Returns True if command exists and can be executed. 243 """Returns True if command is found in the path.
201 """ 244 """
202 # Make sure the command is in the path. 245 if shutil.which(command) is None:
203 res = shutil.which(command)
204 if res is None:
205 cli.log.error("{fg_red}Can't find %s in your path.", command) 246 cli.log.error("{fg_red}Can't find %s in your path.", command)
206 return False 247 return False
248 return True
249
207 250
251def is_executable(command):
252 """Returns True if command can be executed.
253 """
208 # Make sure the command can be executed 254 # Make sure the command can be executed
209 version_arg = ESSENTIAL_BINARIES[command].get('version_arg', '--version') 255 version_arg = ESSENTIAL_BINARIES[command].get('version_arg', '--version')
210 check = cli.run([command, version_arg], combined_output=True, stdin=DEVNULL, timeout=5) 256 check = cli.run([command, version_arg], combined_output=True, stdin=DEVNULL, timeout=5)
diff --git a/lib/python/qmk/cli/doctor/main.py b/lib/python/qmk/cli/doctor/main.py
index 391353ebbf..45667e8ce2 100755
--- a/lib/python/qmk/cli/doctor/main.py
+++ b/lib/python/qmk/cli/doctor/main.py
@@ -3,7 +3,6 @@
3Check out the user's QMK environment and make sure it's ready to compile. 3Check out the user's QMK environment and make sure it's ready to compile.
4""" 4"""
5import platform 5import platform
6from subprocess import DEVNULL
7 6
8from milc import cli 7from milc import cli
9from milc.questions import yesno 8from milc.questions import yesno
@@ -16,6 +15,60 @@ from qmk.commands import in_virtualenv
16from qmk.userspace import qmk_userspace_paths, qmk_userspace_validate, UserspaceValidationError 15from qmk.userspace import qmk_userspace_paths, qmk_userspace_validate, UserspaceValidationError
17 16
18 17
18def distrib_tests():
19 def _load_kvp_file(file):
20 """Load a simple key=value file into a dictionary
21 """
22 vars = {}
23 with open(file, 'r') as f:
24 for line in f:
25 if '=' in line:
26 key, value = line.split('=', 1)
27 vars[key.strip()] = value.strip()
28 return vars
29
30 def _parse_toolchain_release_file(file):
31 """Parse the QMK toolchain release info file
32 """
33 try:
34 vars = _load_kvp_file(file)
35 return f'{vars.get("TOOLCHAIN_HOST", "unknown")}:{vars.get("TOOLCHAIN_TARGET", "unknown")}:{vars.get("COMMIT_HASH", "unknown")}'
36 except Exception as e:
37 cli.log.warning('Error reading QMK toolchain release info file: %s', e)
38 return f'Unknown toolchain release info file: {file}'
39
40 def _parse_flashutils_release_file(file):
41 """Parse the QMK flashutils release info file
42 """
43 try:
44 vars = _load_kvp_file(file)
45 return f'{vars.get("FLASHUTILS_HOST", "unknown")}:{vars.get("COMMIT_HASH", "unknown")}'
46 except Exception as e:
47 cli.log.warning('Error reading QMK flashutils release info file: %s', e)
48 return f'Unknown flashutils release info file: {file}'
49
50 try:
51 from qmk.cli import QMK_DISTRIB_DIR
52 if (QMK_DISTRIB_DIR / 'etc').exists():
53 cli.log.info('Found QMK tools distribution directory: {fg_cyan}%s', QMK_DISTRIB_DIR)
54
55 toolchains = [_parse_toolchain_release_file(file) for file in (QMK_DISTRIB_DIR / 'etc').glob('toolchain_release_*')]
56 if len(toolchains) > 0:
57 cli.log.info('Found QMK toolchains: {fg_cyan}%s', ', '.join(toolchains))
58 else:
59 cli.log.warning('No QMK toolchains manifest found.')
60
61 flashutils = [_parse_flashutils_release_file(file) for file in (QMK_DISTRIB_DIR / 'etc').glob('flashutils_release_*')]
62 if len(flashutils) > 0:
63 cli.log.info('Found QMK flashutils: {fg_cyan}%s', ', '.join(flashutils))
64 else:
65 cli.log.warning('No QMK flashutils manifest found.')
66 except ImportError:
67 cli.log.info('QMK tools distribution not found.')
68
69 return CheckStatus.OK
70
71
19def os_tests(): 72def os_tests():
20 """Determine our OS and run platform specific tests 73 """Determine our OS and run platform specific tests
21 """ 74 """
@@ -124,10 +177,12 @@ def doctor(cli):
124 * [ ] Compile a trivial program with each compiler 177 * [ ] Compile a trivial program with each compiler
125 """ 178 """
126 cli.log.info('QMK Doctor is checking your environment.') 179 cli.log.info('QMK Doctor is checking your environment.')
180 cli.log.info('Python version: %s', platform.python_version())
127 cli.log.info('CLI version: %s', cli.version) 181 cli.log.info('CLI version: %s', cli.version)
128 cli.log.info('QMK home: {fg_cyan}%s', QMK_FIRMWARE) 182 cli.log.info('QMK home: {fg_cyan}%s', QMK_FIRMWARE)
129 183
130 status = os_status = os_tests() 184 status = os_status = os_tests()
185 distrib_tests()
131 186
132 userspace_tests(None) 187 userspace_tests(None)
133 188
@@ -141,12 +196,6 @@ def doctor(cli):
141 196
142 # Make sure the basic CLI tools we need are available and can be executed. 197 # Make sure the basic CLI tools we need are available and can be executed.
143 bin_ok = check_binaries() 198 bin_ok = check_binaries()
144
145 if bin_ok == CheckStatus.ERROR:
146 if yesno('Would you like to install dependencies?', default=True):
147 cli.run(['util/qmk_install.sh', '-y'], stdin=DEVNULL, capture_output=False)
148 bin_ok = check_binaries()
149
150 if bin_ok == CheckStatus.OK: 199 if bin_ok == CheckStatus.OK:
151 cli.log.info('All dependencies are installed.') 200 cli.log.info('All dependencies are installed.')
152 elif bin_ok == CheckStatus.WARNING: 201 elif bin_ok == CheckStatus.WARNING:
@@ -163,7 +212,6 @@ def doctor(cli):
163 212
164 # Check out the QMK submodules 213 # Check out the QMK submodules
165 sub_ok = check_submodules() 214 sub_ok = check_submodules()
166
167 if sub_ok == CheckStatus.OK: 215 if sub_ok == CheckStatus.OK:
168 cli.log.info('Submodules are up to date.') 216 cli.log.info('Submodules are up to date.')
169 else: 217 else:
@@ -186,6 +234,7 @@ def doctor(cli):
186 cli.log.info('{fg_yellow}QMK is ready to go, but minor problems were found') 234 cli.log.info('{fg_yellow}QMK is ready to go, but minor problems were found')
187 return 1 235 return 1
188 else: 236 else:
189 cli.log.info('{fg_red}Major problems detected, please fix these problems before proceeding.') 237 cli.log.info('{fg_red}Major problems detected, please fix these problems before proceeding.{fg_reset}')
190 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.') 238 cli.log.info('{fg_blue}If you\'re missing dependencies, try following the instructions on: https://docs.qmk.fm/newbs_getting_started{fg_reset}')
239 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}')
191 return 2 240 return 2
diff --git a/lib/python/qmk/flashers.py b/lib/python/qmk/flashers.py
index b70b5fb035..6b52f4d35a 100644
--- a/lib/python/qmk/flashers.py
+++ b/lib/python/qmk/flashers.py
@@ -155,10 +155,10 @@ def _flash_atmel_dfu(mcu, file):
155def _flash_hid_bootloader(mcu, details, file): 155def _flash_hid_bootloader(mcu, details, file):
156 cmd = None 156 cmd = None
157 if details == 'halfkay': 157 if details == 'halfkay':
158 if shutil.which('teensy-loader-cli'): 158 if shutil.which('teensy_loader_cli'):
159 cmd = 'teensy-loader-cli'
160 elif shutil.which('teensy_loader_cli'):
161 cmd = 'teensy_loader_cli' 159 cmd = 'teensy_loader_cli'
160 elif shutil.which('teensy-loader-cli'):
161 cmd = 'teensy-loader-cli'
162 162
163 # Use 'hid_bootloader_cli' for QMK HID and as a fallback for HalfKay 163 # Use 'hid_bootloader_cli' for QMK HID and as a fallback for HalfKay
164 if not cmd: 164 if not cmd:
diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py
index a0b8fe72b6..e07fa0ccae 100644
--- a/lib/python/qmk/info.py
+++ b/lib/python/qmk/info.py
@@ -15,7 +15,7 @@ from qmk.json_schema import deep_update, json_load, validate
15from qmk.keyboard import config_h, rules_mk 15from qmk.keyboard import config_h, rules_mk
16from qmk.commands import parse_configurator_json 16from qmk.commands import parse_configurator_json
17from qmk.makefile import parse_rules_mk_file 17from qmk.makefile import parse_rules_mk_file
18from qmk.math import compute 18from qmk.math_ops import compute
19from qmk.util import maybe_exit, truthy 19from qmk.util import maybe_exit, truthy
20 20
21true_values = ['1', 'on', 'yes'] 21true_values = ['1', 'on', 'yes']
diff --git a/lib/python/qmk/keyboard.py b/lib/python/qmk/keyboard.py
index 254dc62309..e8534492c9 100644
--- a/lib/python/qmk/keyboard.py
+++ b/lib/python/qmk/keyboard.py
@@ -175,8 +175,9 @@ def keyboard_completer(prefix, action, parser, parsed_args):
175 return list_keyboards() 175 return list_keyboards()
176 176
177 177
178@lru_cache(maxsize=None)
178def list_keyboards(): 179def list_keyboards():
179 """Returns a list of all keyboards 180 """Returns a list of all keyboards.
180 """ 181 """
181 # We avoid pathlib here because this is performance critical code. 182 # We avoid pathlib here because this is performance critical code.
182 kb_wildcard = os.path.join(base_path, "**", 'keyboard.json') 183 kb_wildcard = os.path.join(base_path, "**", 'keyboard.json')
@@ -184,6 +185,9 @@ def list_keyboards():
184 185
185 found = map(_find_name, paths) 186 found = map(_find_name, paths)
186 187
188 # Convert to posix paths for consistency
189 found = map(lambda x: str(Path(x).as_posix()), found)
190
187 return sorted(set(found)) 191 return sorted(set(found))
188 192
189 193
diff --git a/lib/python/qmk/math.py b/lib/python/qmk/math_ops.py
index 88dc4a300c..1f14b18f4e 100644
--- a/lib/python/qmk/math.py
+++ b/lib/python/qmk/math_ops.py
@@ -23,8 +23,8 @@ def compute(expr):
23 23
24 24
25def _eval(node): 25def _eval(node):
26 if isinstance(node, ast.Num): # <number> 26 if isinstance(node, ast.Constant): # <number>
27 return node.n 27 return node.value
28 elif isinstance(node, ast.BinOp): # <left> <operator> <right> 28 elif isinstance(node, ast.BinOp): # <left> <operator> <right>
29 return operators[type(node.op)](_eval(node.left), _eval(node.right)) 29 return operators[type(node.op)](_eval(node.left), _eval(node.right))
30 elif isinstance(node, ast.UnaryOp): # <operator> <operand> e.g., -1 30 elif isinstance(node, ast.UnaryOp): # <operator> <operand> e.g., -1
diff --git a/platforms/avr/flash.mk b/platforms/avr/flash.mk
index 51731f0aa8..bfb292224c 100644
--- a/platforms/avr/flash.mk
+++ b/platforms/avr/flash.mk
@@ -5,10 +5,10 @@
5 5
6# Autodetect teensy loader 6# Autodetect teensy loader
7ifndef TEENSY_LOADER_CLI 7ifndef TEENSY_LOADER_CLI
8 ifneq (, $(shell which teensy-loader-cli 2>/dev/null)) 8 ifneq (, $(shell which teensy_loader_cli 2>/dev/null))
9 TEENSY_LOADER_CLI ?= teensy-loader-cli
10 else
11 TEENSY_LOADER_CLI ?= teensy_loader_cli 9 TEENSY_LOADER_CLI ?= teensy_loader_cli
10 else
11 TEENSY_LOADER_CLI ?= teensy-loader-cli
12 endif 12 endif
13endif 13endif
14 14
diff --git a/platforms/chibios/flash.mk b/platforms/chibios/flash.mk
index f4db17a58b..0734754834 100644
--- a/platforms/chibios/flash.mk
+++ b/platforms/chibios/flash.mk
@@ -77,10 +77,10 @@ st-flash: $(BUILD_DIR)/$(TARGET).hex sizeafter
77 77
78# Autodetect teensy loader 78# Autodetect teensy loader
79ifndef TEENSY_LOADER_CLI 79ifndef TEENSY_LOADER_CLI
80 ifneq (, $(shell which teensy-loader-cli 2>/dev/null)) 80 ifneq (, $(shell which teensy_loader_cli 2>/dev/null))
81 TEENSY_LOADER_CLI ?= teensy-loader-cli
82 else
83 TEENSY_LOADER_CLI ?= teensy_loader_cli 81 TEENSY_LOADER_CLI ?= teensy_loader_cli
82 else
83 TEENSY_LOADER_CLI ?= teensy-loader-cli
84 endif 84 endif
85endif 85endif
86 86
diff --git a/util/env-bootstrap.sh b/util/env-bootstrap.sh
new file mode 100755
index 0000000000..6b0497ffae
--- /dev/null
+++ b/util/env-bootstrap.sh
@@ -0,0 +1,594 @@
1#!/usr/bin/env sh
2# Copyright 2025 Nick Brassel (@tzarc)
3# SPDX-License-Identifier: GPL-2.0-or-later
4
5################################################################################
6# This script will install the QMK CLI, toolchains, and flashing utilities.
7################################################################################
8# Environment variables:
9# CONFIRM: Skip the pre-install delay. (or: --confirm)
10# QMK_DISTRIB_DIR: The directory to install the QMK distribution to. (or: --qmk-distrib-dir=...)
11# UV_INSTALL_DIR: The directory to install `uv` to. (or: --uv-install-dir=...)
12# UV_TOOL_DIR: The directory to install `uv` tools to. (or: --uv-tool-dir=...)
13# SKIP_CLEAN: Skip cleaning the distribution directory. (or: --skip-clean)
14# SKIP_PACKAGE_MANAGER: Skip installing the necessary packages for the package manager. (or: --skip-package-manager)
15# SKIP_UV: Skip installing `uv`. (or: --skip-uv)
16# SKIP_QMK_CLI: Skip installing the QMK CLI. (or: --skip-qmk-cli)
17# SKIP_QMK_TOOLCHAINS: Skip installing the QMK toolchains. (or: --skip-qmk-toolchains)
18# SKIP_QMK_FLASHUTILS: Skip installing the QMK flashing utilities. (or: --skip-qmk-flashutils)
19# SKIP_UDEV_RULES: Skip installing the udev rules for Linux. (or: --skip-udev-rules)
20# SKIP_WINDOWS_DRIVERS: Skip installing the Windows drivers for the flashing utilities. (or: --skip-windows-drivers)
21#
22# Arguments above may be negated by prefixing with `--no-` instead (e.g. `--no-skip-clean`).
23################################################################################
24# Usage:
25# curl -fsSL https://raw.githubusercontent.com/qmk/qmk_firmware/master/util/env-bootstrap.sh | sh
26#
27# Help:
28# curl -fsSL https://raw.githubusercontent.com/qmk/qmk_firmware/master/util/env-bootstrap.sh | sh -s -- --help
29#
30# An example which skips installing `uv` using environment variables:
31# curl -fsSL https://raw.githubusercontent.com/qmk/qmk_firmware/master/util/env-bootstrap.sh | SKIP_UV=1 sh
32#
33# ...or by using command line arguments:
34# curl -fsSL https://raw.githubusercontent.com/qmk/qmk_firmware/master/util/env-bootstrap.sh | sh -s -- --skip-uv
35#
36# Any other configurable items listed above may be specified in the same way.
37################################################################################
38
39{ # this ensures the entire script is downloaded #
40 set -eu
41
42 BOOTSTRAP_TMPDIR="$(mktemp -d /tmp/qmk-bootstrap-failure.XXXXXX)"
43 trap 'rm -rf "$BOOTSTRAP_TMPDIR" >/dev/null 2>&1 || true' EXIT
44 FAILURE_FILE="${BOOTSTRAP_TMPDIR}/fail"
45
46 # Work out which `sed` to use
47 command -v gsed >/dev/null 2>&1 && SED=gsed || SED=sed
48
49 script_args() {
50 cat <<__EOT__
51 --help -- Shows this help text
52 --confirm -- Skips the delay before installation
53 --uv-install-dir={path} -- The directory to install \`uv\` into
54 --uv-tool-dir={path} -- The directory to install \`uv\` tools into
55 --qmk-distrib-dir={path} -- The directory to install the QMK distribution into
56 --skip-clean -- Skip cleaning the QMK distribution directory
57 --skip-package-manager -- Skip installing the necessary packages for the package manager
58 --skip-uv -- Skip installing \`uv\`
59 --skip-qmk-cli -- Skip installing the QMK CLI
60 --skip-qmk-toolchains -- Skip installing the QMK toolchains
61 --skip-qmk-flashutils -- Skip installing the QMK flashing utilities
62 --skip-udev-rules -- Skip installing the udev rules for Linux
63 --skip-windows-drivers -- Skip installing the Windows drivers for the flashing utilities
64__EOT__
65 # Hidden:
66 # --wsl-install -- Installs the WSL variant of qmk_flashutils
67 }
68
69 signal_execution_failure() {
70 touch "$FAILURE_FILE" >/dev/null 2>&1 || true
71 }
72
73 exit_if_execution_failed() {
74 if [ -e "$FAILURE_FILE" ]; then
75 exit 1
76 fi
77 }
78
79 script_help() {
80 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' ' ')"
81 echo
82 echo "Arguments:"
83 script_args
84 echo
85 echo "Switch arguments may be negated by prefixing with '--no-' (e.g. '--no-skip-clean')."
86 }
87
88 script_parse_args() {
89 local N
90 local V
91 while [ ! -z "${1:-}" ]; do
92 case "$1" in
93 --help)
94 script_help
95 exit 0
96 ;;
97 --*=*)
98 N=${1%%=*}
99 N=${N##--}
100 N=$(echo $N | tr '-' '_' | tr 'a-z' 'A-Z')
101 V=${1##*=}
102 export $N="$V"
103 ;;
104 --no-*)
105 N=${1##--no-}
106 N=$(echo $N | tr '-' '_' | tr 'a-z' 'A-Z')
107 unset $N
108 ;;
109 --*)
110 N=${1##--}
111 N=$(echo $N | tr '-' '_' | tr 'a-z' 'A-Z')
112 export $N=true
113 ;;
114 *)
115 echo "Unknown argument: '$1'" >&2
116 echo
117 script_help >&2
118 exit 1
119 ;;
120 esac
121 shift
122 unset N
123 unset V
124 done
125 }
126
127 nsudo() {
128 if [ "$(fn_os)" = "windows" ]; then
129 # No need for sudo under QMK MSYS
130 return
131 elif [ $(id -u) -ne 0 ]; then
132 if [ -n "$(command -v sudo 2>/dev/null || true)" ]; then
133 echo "sudo"
134 elif [ -n "$(command -v doas 2>/dev/null || true)" ]; then
135 echo "doas"
136 else
137 echo "Please install 'sudo' or 'doas' to continue." >&2
138 exit 1
139 fi
140 fi
141 true
142 }
143
144 download_url() {
145 local url=$1
146 local filename=${2:-$(basename "$url")}
147 local quiet=''
148 if [ -n "$(command -v curl 2>/dev/null || true)" ]; then
149 [ "$filename" = "-" ] && quiet='-s' || echo "Downloading '$url' => '$filename'" >&2
150 curl -LSf $quiet -o "$filename" "$url"
151 elif [ -n "$(command -v wget 2>/dev/null || true)" ]; then
152 [ "$filename" = "-" ] && quiet='-q' || echo "Downloading '$url' => '$filename'" >&2
153 wget $quiet "-O$filename" "$url"
154 else
155 echo "Please install 'curl' to continue." >&2
156 exit 1
157 fi
158 }
159
160 github_api_call() {
161 local url="$1"
162 local token="${GITHUB_TOKEN:-${GH_TOKEN:-}}"
163 if [ -n "${token:-}" ]; then
164 if [ -n "$(command -v curl 2>/dev/null || true)" ]; then
165 curl -fsSL -H "Authorization: token $token" -H "Accept: application/vnd.github.v3+json" "https://api.github.com/$url"
166 elif [ -n "$(command -v wget 2>/dev/null || true)" ]; then
167 wget -q --header="Authorization: token $token" --header="Accept: application/vnd.github.v3+json" "https://api.github.com/$url" -O -
168 fi
169 else
170 download_url "https://api.github.com/$url" -
171 fi
172 }
173
174 fn_os() {
175 local os_name=$(echo ${1:-} | tr 'A-Z' 'a-z')
176 if [ -z "$os_name" ]; then
177 os_name=$(uname -s | tr 'A-Z' 'a-z')
178 fi
179 case "$os_name" in
180 *darwin* | *macos* | *apple*)
181 echo macos
182 ;;
183 *windows* | *mingw* | *msys*)
184 echo windows
185 ;;
186 *linux*)
187 echo linux
188 ;;
189 *)
190 echo unknown
191 ;;
192 esac
193 }
194
195 fn_arch() {
196 local arch_name=$(echo ${1:-} | tr 'A-Z' 'a-z')
197 if [ -z "$arch_name" ]; then
198 arch_name=$(uname -m | tr 'A-Z' 'a-z')
199 fi
200 case "$arch_name" in
201 *arm64* | *aarch64*)
202 echo ARM64
203 ;;
204 *riscv64*)
205 echo RV64
206 ;;
207 *x86_64* | *x64*)
208 echo X64
209 ;;
210 *)
211 echo unknown
212 ;;
213 esac
214 }
215
216 preinstall_delay() {
217 [ -z "${CONFIRM:-}" ] || return 0
218 echo >&2
219 echo "Waiting 10 seconds before proceeding. Press Ctrl+C to cancel installation." >&2
220 sleep 10
221 }
222
223 get_package_manager_deps() {
224 case $(fn_os) in
225 macos) echo "zstd clang-format make hidapi libusb dos2unix git" ;;
226 windows) echo "base-devel: zstd:p toolchain:p clang:p hidapi:p dos2unix: git: unzip:" ;;
227 linux)
228 case $(grep ID /etc/os-release) in
229 *arch* | *manjaro* | *cachyos*) echo "zstd base-devel clang diffutils wget unzip zip hidapi dos2unix git" ;;
230 *debian* | *ubuntu*) echo "zstd build-essential clang-format diffutils wget unzip zip libhidapi-hidraw0 dos2unix git" ;;
231 *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" ;;
232 *suse*) echo "zstd clang diffutils wget unzip zip libhidapi-hidraw0 dos2unix git libusb-1_0-devel gzip which" ;;
233 *gentoo*) echo "zstd diffutils wget unzip zip dev-libs/hidapi dos2unix dev-vcs/git dev-libs/libusb app-arch/gzip which" ;;
234 *)
235 echo >&2
236 echo "Sorry, we don't recognize your distribution." >&2
237 echo >&2
238 echo "Proceeding with the installation, however you will need to install at least the following tools manually:" >&2
239 echo " - make, git, curl, zstd, unzip, [lib]hidapi" >&2
240 echo "Other tools may be required depending on your distribution." >&2
241 echo >&2
242 echo "Alternatively, if you prefer Docker, try using the docker image instead:" >&2
243 echo " - https://docs.qmk.fm/#/getting_started_docker" >&2
244 ;;
245 esac
246 ;;
247 *)
248 # We can only really support macOS, Windows, and Linux at this time due to `uv` requirements.
249 echo >&2
250 echo "Sorry, we don't recognize your OS. Try using a compatible OS instead:" >&2
251 echo " - https://docs.qmk.fm/newbs_getting_started#set-up-your-environment" >&2
252 echo >&2
253 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
254 echo " % python3 -m pip install qmk" >&2
255 echo >&2
256 echo "All other dependencies will need to be installed manually, such as make, git, AVR and ARM toolchains, and associated flashing utilities." >&2
257 echo >&2
258 echo "**NOTE**: QMK does not provide official support for your environment. Here be dragons, you are on your own." >&2
259 signal_execution_failure
260 ;;
261 esac
262 }
263
264 print_package_manager_deps_and_delay() {
265 get_package_manager_deps | tr ' ' '\n' | sort | xargs -I'{}' echo " - {}" >&2
266 exit_if_execution_failed
267 preinstall_delay || exit 1
268 }
269
270 install_package_manager_deps() {
271 # Install the necessary packages for the package manager
272 case $(fn_os) in
273 macos)
274 if [ -n "$(command -v brew 2>/dev/null || true)" ]; then
275 echo "It will also install the following system packages using 'brew':" >&2
276 print_package_manager_deps_and_delay
277
278 brew update
279
280 local existing=""
281 local new=""
282 for dep in $(get_package_manager_deps); do
283 if brew list --formula | grep -q "^${dep}\$"; then
284 existing="${existing:-} $dep"
285 else
286 new="${new:-} $dep"
287 fi
288 done
289
290 if [ -n "${existing:-}" ]; then
291 brew upgrade $existing
292 fi
293 if [ -n "${new:-}" ]; then
294 brew install $new
295 fi
296 else
297 echo "Please install 'brew' to continue. See https://brew.sh/ for more information." >&2
298 exit 1
299 fi
300 ;;
301 windows)
302 echo "It will also install the following packages using 'pacman'/'pacboy':" >&2
303 print_package_manager_deps_and_delay
304 $(nsudo) pacman --needed --noconfirm --disable-download-timeout -S pactoys
305 $(nsudo) pacboy sync --needed --noconfirm --disable-download-timeout $(get_package_manager_deps)
306 ;;
307 linux)
308 case $(grep ID /etc/os-release) in
309 *arch* | *manjaro* | *cachyos*)
310 echo "It will also install the following system packages using 'pacman':" >&2
311 print_package_manager_deps_and_delay
312 $(nsudo) pacman --needed --noconfirm -S $(get_package_manager_deps)
313 ;;
314 *debian* | *ubuntu*)
315 echo "It will also install the following system packages using 'apt':" >&2
316 print_package_manager_deps_and_delay
317 $(nsudo) apt-get update
318 DEBIAN_FRONTEND=noninteractive \
319 $(nsudo) apt-get --quiet --yes install $(get_package_manager_deps)
320 ;;
321 *fedora*)
322 echo "It will also install the following system packages using 'dnf':" >&2
323 print_package_manager_deps_and_delay
324 # Some RHEL-likes need EPEL for hidapi
325 $(nsudo) dnf -y install epel-release 2>/dev/null || true
326 # RHEL-likes have some naming differences in libusb packages, so manually handle those
327 $(nsudo) dnf -y install $(get_package_manager_deps | tr ' ' '\n' | grep -v 'epel-release' | grep -v libusb | tr '\n' ' ')
328 for pkg in $(get_package_manager_deps | tr ' ' '\n' | grep libusb); do
329 $(nsudo) dnf -y install "$pkg" 2>/dev/null || true
330 done
331 ;;
332 *opensuse* | *suse*)
333 echo "It will also install development tools as well as the following system packages using 'zypper':" >&2
334 print_package_manager_deps_and_delay
335 $(nsudo) zypper --non-interactive refresh
336 $(nsudo) zypper --non-interactive install -t pattern devel_basis devel_C_C++
337 $(nsudo) zypper --non-interactive install $(get_package_manager_deps)
338 ;;
339 *gentoo*)
340 echo "It will also install the following system packages using 'emerge':" >&2
341 print_package_manager_deps_and_delay
342 $(nsudo) emerge --sync
343 $(nsudo) emerge --noreplace --ask=n $(get_package_manager_deps | tr ' ' '\n') || signal_execution_failure
344 exit_if_execution_failed
345 ;;
346 *)
347 print_package_manager_deps_and_delay
348 echo "Proceeding with the installation, you will need to ensure prerequisites are installed." >&2
349 ;;
350 esac
351 ;;
352 *)
353 print_package_manager_deps_and_delay
354 ;;
355 esac
356 }
357
358 install_uv() {
359 # Install `uv` (or update as necessary)
360 download_url https://astral.sh/uv/install.sh - | TMPDIR="$(windows_ish_path "${TMPDIR:-}")" UV_INSTALL_DIR="$(windows_ish_path "${UV_INSTALL_DIR:-}")" sh
361 }
362
363 setup_paths() {
364 # Set up the paths for any of the locations `uv` expects
365 if [ -n "${XDG_BIN_HOME:-}" ]; then
366 export PATH="$XDG_BIN_HOME:$PATH"
367 fi
368 if [ -n "${XDG_DATA_HOME:-}" ]; then
369 export PATH="$XDG_DATA_HOME/../bin:$PATH"
370 fi
371 [ ! -d "$HOME/.local/bin" ] || export PATH="$HOME/.local/bin:$PATH"
372
373 if [ -n "${UV_INSTALL_DIR:-}" ]; then
374 export PATH="$UV_INSTALL_DIR/bin:$UV_INSTALL_DIR:$PATH" # cater for both "flat" and "hierarchical" installs of `uv`
375 fi
376
377 if [ -n "${UV_TOOL_BIN_DIR:-}" ]; then
378 export PATH="$UV_TOOL_BIN_DIR:$PATH"
379 fi
380 }
381
382 uv_command() {
383 if [ "$(fn_os)" = "windows" ]; then
384 UV_TOOL_DIR="$(windows_ish_path "${UV_TOOL_DIR:-}")" \
385 UV_TOOL_BIN_DIR="$(windows_ish_path "${UV_TOOL_BIN_DIR:-}")" \
386 uv "$@"
387 else
388 uv "$@"
389 fi
390 }
391
392 install_qmk_cli() {
393 # Install the QMK CLI
394 uv_command tool install --force --with pip --upgrade --python $PYTHON_TARGET_VERSION qmk
395
396 # QMK is installed to...
397 local qmk_tooldir="$(posix_ish_path "$(uv_command tool dir)/qmk")"
398
399 # Activate the environment
400 if [ -e "$qmk_tooldir/bin" ]; then
401 . "$qmk_tooldir/bin/activate"
402 elif [ -e "$qmk_tooldir/Scripts" ]; then
403 . "$qmk_tooldir/Scripts/activate"
404 else
405 echo "Could not find the QMK environment to activate." >&2
406 exit 1
407 fi
408
409 # Install the QMK dependencies
410 uv_command pip install --upgrade -r https://raw.githubusercontent.com/qmk/qmk_firmware/refs/heads/master/requirements.txt
411 uv_command pip install --upgrade -r https://raw.githubusercontent.com/qmk/qmk_firmware/refs/heads/master/requirements-dev.txt
412
413 # Deactivate the environment
414 deactivate
415 }
416
417 install_toolchains() {
418 # Get the latest toolchain release from https://github.com/qmk/qmk_toolchains
419 local latest_toolchains_release=$(github_api_call repos/qmk/qmk_toolchains/releases/latest - | grep -oE '"tag_name": "[^"]+' | grep -oE '[^"]+$')
420 # Download the specific release asset with a matching keyword
421 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))
422 if [ -z "$toolchain_url" ]; then
423 echo "No toolchain found for this OS/Arch combination." >&2
424 exit 1
425 fi
426
427 # Download the toolchain release to the toolchains location
428 echo "Downloading compiler toolchains..." >&2
429 local target_file="$QMK_DISTRIB_DIR/$(basename "$toolchain_url")"
430 download_url "$toolchain_url" "$target_file"
431
432 # Extract the toolchain
433 echo "Extracting compiler toolchains to '$QMK_DISTRIB_DIR'..." >&2
434 zstdcat "$target_file" | tar xf - -C "$QMK_DISTRIB_DIR" --strip-components=1
435 }
436
437 install_flashing_tools() {
438 local osarchvariant="$(fn_os)$(fn_arch)"
439
440 # Special case for WSL
441 if [ -n "${WSL_INSTALL:-}" ] || [ -n "${WSL_DISTRO_NAME:-}" ] || [ -f /proc/sys/fs/binfmt_misc/WSLInterop ]; then
442 osarchvariant="windowsWSL"
443 fi
444
445 # Get the latest flashing tools release from https://github.com/qmk/qmk_flashutils
446 local latest_flashutils_release=$(github_api_call repos/qmk/qmk_flashutils/releases/latest - | grep -oE '"tag_name": "[^"]+' | grep -oE '[^"]+$')
447 # Download the specific release asset with a matching keyword
448 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")
449 if [ -z "$flashutils_url" ]; then
450 echo "No flashing tools found for this OS/Arch combination." >&2
451 exit 1
452 fi
453
454 # Download the flashing tools release to the toolchains location
455 echo "Downloading flashing tools..." >&2
456 local target_file="$QMK_DISTRIB_DIR/$(basename "$flashutils_url")"
457 download_url "$flashutils_url" "$target_file"
458
459 # Extract the flashing tools
460 echo "Extracting flashing tools to '$QMK_DISTRIB_DIR'..." >&2
461 zstdcat "$target_file" | tar xf - -C "$QMK_DISTRIB_DIR/bin"
462 # Move the release file to etc
463 mv "$QMK_DISTRIB_DIR/bin/flashutils_release"* "$QMK_DISTRIB_DIR/etc"
464 }
465
466 install_linux_udev_rules() {
467 # Download the udev rules to the toolchains location
468 echo "Downloading QMK udev rules file..." >&2
469 local qmk_rules_target_file="$QMK_DISTRIB_DIR/50-qmk.rules"
470 download_url "https://raw.githubusercontent.com/qmk/qmk_firmware/refs/heads/master/util/udev/50-qmk.rules" "$qmk_rules_target_file"
471
472 # Install the udev rules -- path list is aligned with qmk doctor's linux.py
473 local udev_rules_paths="
474 /usr/lib/udev/rules.d
475 /usr/local/lib/udev/rules.d
476 /run/udev/rules.d
477 /etc/udev/rules.d
478 "
479 for udev_rules_dir in $udev_rules_paths; do
480 if [ -d "$udev_rules_dir" ]; then
481 echo "Installing udev rules to $udev_rules_dir/50-qmk.rules ..." >&2
482 $(nsudo) mv "$qmk_rules_target_file" "$udev_rules_dir"
483 $(nsudo) chown 0:0 "$udev_rules_dir/50-qmk.rules"
484 $(nsudo) chmod 644 "$udev_rules_dir/50-qmk.rules"
485 break
486 fi
487 done
488
489 # Reload udev rules
490 if command -v udevadm >/dev/null 2>&1; then
491 echo "Reloading udev rules..." >&2
492 $(nsudo) udevadm control --reload-rules || true
493 $(nsudo) udevadm trigger || true
494 else
495 echo "udevadm not found, skipping udev rules reload." >&2
496 fi
497 }
498
499 install_windows_drivers() {
500 # Get the latest driver installer release from https://github.com/qmk/qmk_driver_installer
501 local latest_driver_installer_release=$(github_api_call repos/qmk/qmk_driver_installer/releases/latest - | grep -oE '"tag_name": "[^"]+' | grep -oE '[^"]+$')
502 # Download the specific release asset
503 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')
504 if [ -z "$driver_installer_url" ]; then
505 echo "No driver installer found." >&2
506 exit 1
507 fi
508 # Download the driver installer release to the toolchains location
509 echo "Downloading driver installer..." >&2
510 local target_file="$QMK_DISTRIB_DIR/$(basename "$driver_installer_url")"
511 download_url "$driver_installer_url" "$target_file"
512 # Download the drivers list
513 download_url "https://raw.githubusercontent.com/qmk/qmk_firmware/refs/heads/master/util/drivers.txt" "$QMK_DISTRIB_DIR/drivers.txt"
514 # Execute the driver installer
515 cd "$QMK_DISTRIB_DIR"
516 cmd.exe //c "qmk_driver_installer.exe --all --force drivers.txt"
517 cd -
518 # Remove the temporary files
519 rm -f "$QMK_DISTRIB_DIR/qmk_driver_installer.exe" "$QMK_DISTRIB_DIR/drivers.txt" || true
520 }
521
522 clean_tarballs() {
523 # Clean up the tarballs
524 rm -f "$QMK_DISTRIB_DIR"/*.tar.zst || true
525 }
526
527 windows_ish_path() {
528 [ -n "$1" ] || return 0
529 [ "$(uname -o 2>/dev/null || true)" = "Msys" ] && cygpath -w "$1" || echo "$1"
530 }
531
532 posix_ish_path() {
533 [ -n "$1" ] || return 0
534 [ "$(uname -o 2>/dev/null || true)" = "Msys" ] && cygpath -u "$1" || echo "$1"
535 }
536
537 # Set the Python version we want to use with the QMK CLI
538 export PYTHON_TARGET_VERSION=${PYTHON_TARGET_VERSION:-3.14}
539
540 # Windows/MSYS doesn't like `/tmp` so we need to set a different temporary directory.
541 # 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.
542 if [ "$(uname -o 2>/dev/null || true)" = "Msys" ]; then
543 export TMPDIR="$(posix_ish_path "$TMP")"
544 export UV_INSTALL_DIR="$(posix_ish_path "${UV_INSTALL_DIR:-/opt/uv}")"
545 export QMK_DISTRIB_DIR="$(posix_ish_path "${QMK_DISTRIB_DIR:-/opt/qmk}")"
546 export UV_TOOL_DIR="$(posix_ish_path "${UV_TOOL_DIR:-"$UV_INSTALL_DIR/tools"}")"
547 export UV_TOOL_BIN_DIR="$(posix_ish_path "$UV_TOOL_DIR/bin")"
548 fi
549
550 script_parse_args "$@"
551
552 echo "This QMK CLI installation script will install \`uv\`, the QMK CLI, as well as QMK-supplied toolchains and flashing utilities." >&2
553 [ -z "${SKIP_PACKAGE_MANAGER:-}" ] || { preinstall_delay || exit 1; }
554 [ -n "${SKIP_PACKAGE_MANAGER:-}" ] || install_package_manager_deps
555 [ -n "${SKIP_UV:-}" ] || install_uv
556
557 # Make sure the usual `uv` and other associated directories are on the $PATH
558 setup_paths
559
560 # Work out where we want to install the distribution and tools now that `uv` is installed
561 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 -)}")"
562
563 # Clear out the distrib directory if necessary
564 if [ -z "${SKIP_CLEAN:-}" ] || [ -z "${SKIP_QMK_TOOLCHAINS:-}" -a -z "${SKIP_QMK_FLASHUTILS:-}" ]; then
565 if [ -d "$QMK_DISTRIB_DIR" ]; then
566 echo "Removing old QMK distribution..." >&2
567 rm -rf "$QMK_DISTRIB_DIR"
568 fi
569 fi
570 mkdir -p "$QMK_DISTRIB_DIR"
571
572 [ -n "${SKIP_QMK_CLI:-}" ] || install_qmk_cli
573 [ -n "${SKIP_QMK_TOOLCHAINS:-}" ] || install_toolchains
574 [ -n "${SKIP_QMK_FLASHUTILS:-}" ] || install_flashing_tools
575 if [ "$(uname -s 2>/dev/null || true)" = "Linux" ]; then
576 [ -n "${SKIP_UDEV_RULES:-}" ] || install_linux_udev_rules
577 fi
578 if [ "$(uname -o 2>/dev/null || true)" = "Msys" ]; then
579 [ -n "${SKIP_WINDOWS_DRIVERS:-}" ] || install_windows_drivers
580 fi
581 clean_tarballs
582
583 # Notify the user that they may need to restart their shell to get the `qmk` command
584 echo >&2
585 echo "QMK CLI installation complete." >&2
586 echo "The QMK CLI has been installed to '$(posix_ish_path "$(dirname "$(command -v qmk)")")'." >&2
587 echo "The QMK CLI venv has been created at '$(posix_ish_path "$(uv_command tool dir)/qmk")'." >&2
588 echo "Toolchains and flashing utilities have been installed to '$QMK_DISTRIB_DIR'." >&2
589 echo >&2
590 echo "You may need to restart your shell to gain access to the 'qmk' command." >&2
591 echo "Alternatively, add "$(posix_ish_path "$(dirname "$(command -v qmk)")")" to your \$PATH:" >&2
592 echo " export PATH=\"$(posix_ish_path "$(dirname "$(command -v qmk)")"):\$PATH\"" >&2
593
594} # this ensures the entire script is downloaded #