Compare commits

...
Sign in to create a new pull request.
Author SHA1 Message Date
Nick Brassel
601a58b4cc Merge remote-tracking branch 'upstream/develop' into bootstrap 2025-04-02 13:13:41 +11:00
Nick Brassel
0d18136fdb
Use --confirm or CONFIRM=1 to skip delay, fixup use of sudo when unavailable as root. 2025-03-30 00:28:06 +11:00
Nick Brassel
73b0a6d37c
GHA. 2025-03-30 00:24:12 +11:00
Nick Brassel
99bf22a50e
Windows doesn't like /dev/tty, pivot to delay with Ctrl-C prompt. 2025-03-28 22:52:52 +11:00
Nick Brassel
1921a4808f
Formatting. 2025-03-27 22:39:55 +11:00
Nick Brassel
4ef00cd6e8
Argument parsing. 2025-03-27 22:36:37 +11:00
Nick Brassel
fa170a7c15
Basic package manager stuff on macOS/Linux. 2025-03-27 21:41:02 +11:00
Nick Brassel
6c92b08d8f
Update __init__.py 2025-03-21 00:07:35 +11:00
Nick Brassel
ad8c332b59
Distribution path. 2025-03-20 23:53:19 +11:00
Nick Brassel
99f2b767be
More essential binaries. 2025-03-19 14:01:02 +11:00
Nick Brassel
ddd0f54c94
Hook up internals of the qmk CLI to use $QMK_DISTRIB_DIR. 2025-03-19 12:15:13 +11:00
Nick Brassel
cb4043ec4c
Add environment bootstrap script. 2025-03-19 12:05:06 +11:00
3 changed files with 445 additions and 8 deletions

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. We list each subcommand here explicitly because all the reliable ways of searching for modules are slow and delay startup.
""" """
import os import os
import platform
import platformdirs
import shlex import shlex
import sys import sys
from importlib.util import find_spec from importlib.util import find_spec
@ -12,6 +14,12 @@ from subprocess import run
from milc import cli, __VERSION__ from milc import cli, __VERSION__
from milc.questions import yesno from milc.questions import yesno
# Ensure the QMK distribution is on the `$PATH` if present.
_default_distrib_path = '/opt/qmk' if 'windows' in platform.platform().lower() else platformdirs.user_data_dir('qmk') # this must be kept in sync with the default values inside `util/env-bootstrap.sh`!
QMK_DISTRIB_DIR = Path(os.environ.get('QMK_DISTRIB_DIR', _default_distrib_path))
if QMK_DISTRIB_DIR.exists():
os.environ['PATH'] = str(QMK_DISTRIB_DIR / 'bin') + os.pathsep + os.environ['PATH']
import_names = { import_names = {
# A mapping of package name to importable name # A mapping of package name to importable name
'pep8-naming': 'pep8ext_naming', 'pep8-naming': 'pep8ext_naming',

View file

@ -1,7 +1,6 @@
"""Check for specific programs. """Check for specific programs.
""" """
from enum import Enum from enum import Enum
import re
import shutil import shutil
from subprocess import DEVNULL, TimeoutExpired from subprocess import DEVNULL, TimeoutExpired
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
@ -18,6 +17,18 @@ class CheckStatus(Enum):
ESSENTIAL_BINARIES = { ESSENTIAL_BINARIES = {
'make': {
'version_arg': '--version'
},
'git': {
'version_arg': '--version'
},
'dos2unix': {
'version_arg': '--version'
},
'diff': {
'version_arg': '--version'
},
'dfu-programmer': {}, 'dfu-programmer': {},
'avrdude': {}, 'avrdude': {},
'dfu-util': {}, 'dfu-util': {},
@ -30,14 +41,36 @@ ESSENTIAL_BINARIES = {
} }
def _parse_gcc_version(version): def _check_make_version():
m = re.match(r"(\d+)(?:\.(\d+))?(?:\.(\d+))?", version) last_line = ESSENTIAL_BINARIES['make']['output'].split('\n')[0]
version_number = last_line.split()[2]
cli.log.info('Found make version %s', version_number)
return { return CheckStatus.OK
'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, 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]
version_number = last_line.split()[3]
cli.log.info('Found diff version %s', version_number)
return CheckStatus.OK
def _check_arm_gcc_version(): def _check_arm_gcc_version():
@ -159,6 +192,10 @@ def check_binary_versions():
"""Check the versions of ESSENTIAL_BINARIES """Check the versions of ESSENTIAL_BINARIES
""" """
checks = { checks = {
'make': _check_make_version,
'git': _check_git_version,
'dos2unix': _check_dos2unix_version,
'diff': _check_diff_version,
'arm-none-eabi-gcc': _check_arm_gcc_version, 'arm-none-eabi-gcc': _check_arm_gcc_version,
'avr-gcc': _check_avr_gcc_version, 'avr-gcc': _check_avr_gcc_version,
'avrdude': _check_avrdude_version, 'avrdude': _check_avrdude_version,

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

@ -0,0 +1,392 @@
#!/usr/bin/env bash
# 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=...)
# 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)
#
# 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
# 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
--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
__EOT__
}
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() {
[[ ${EUID} -ne 0 ]] && echo "sudo"
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' or 'wget' to continue." >&2
exit 1
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* | *w64*)
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
}
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
echo " zstd clang-format make hidapi libusb" >&2
preinstall_delay || exit 1
brew update && brew upgrade --formulae
brew install zstd clang-format make hidapi libusb
else
echo "Please install 'brew' to continue. See https://brew.sh/ for more information." >&2
exit 1
fi
;;
linux)
case $(grep ID /etc/os-release) in
*arch* | *manjaro*)
echo "It will also install the following system packages using 'pacman':" >&2
echo " zstd base-devel clang diffutils unzip wget zip hidapi" >&2
preinstall_delay || exit 1
$(nsudo) pacman --needed --noconfirm -S zstd base-devel clang diffutils unzip wget zip
$(nsudo) pacman --needed --noconfirm -S hidapi || true # This will fail if the community repo isn't enabled
;;
*debian* | *ubuntu*)
echo "It will also install the following system packages using 'apt':" >&2
echo " zstd build-essential clang-format diffutils unzip wget zip libhidapi-hidraw0" >&2
preinstall_delay || exit 1
$(nsudo) apt-get update
DEBIAN_FRONTEND=noninteractive \
$(nsudo) apt-get --quiet --yes install zstd build-essential clang-format diffutils unzip wget zip libhidapi-hidraw0
;;
*fedora*)
echo "It will also install the following system packages using 'dnf':" >&2
echo " zstd clang diffutils gcc git unzip wget zip hidapi" >&2
echo "And whichever of the following is available, depending on which packages are provided by the distro:" >&2
echo " libusb-devel, libusb1-devel, libusb-compat-0.1-devel, or libusb0-devel" >&2
preinstall_delay || exit 1
$(nsudo) dnf -y install zstd clang diffutils gcc git unzip wget zip hidapi libusb-devel libusb1-devel libusb-compat-0.1-devel libusb0-devel --skip-unavailable
;;
*gentoo*)
echo "It will also the following packages using 'emerge':" >&2
echo " app-arch/zstd app-arch/unzip app-arch/zip net-misc/wget llvm-core/clang sys-apps/hwloc dev-libs/hidapi" >&2
preinstall_delay || exit 1
$(nsudo) emerge -au --noreplace \
app-arch/zstd app-arch/unzip app-arch/zip net-misc/wget llvm-core/clang sys-apps/hwloc dev-libs/hidapi
;;
*slackware*)
echo "It will also the following packages using 'sboinstall':" >&2
echo " python3" >&2
preinstall_delay || exit 1
$(nsudo) sboinstall python3 # Rest tbd?
;;
*solus*)
echo "It will also install the following system packages using 'eopkg':" >&2
echo " system.devel zstd git wget zip unzip python3" >&2
preinstall_delay || exit 1
$(nsudo) eopkg -y update-repo
$(nsudo) eopkg -y upgrade
$(nsudo) eopkg -y install \
-c system.devel zstd git wget zip unzip python3
;;
*void*)
echo "It will also the following packages using 'xbps-install':" >&2
echo " zstd git make wget unzip zip python3" >&2
preinstall_delay || exit 1
$(nsudo) xbps-install -y \
zstd git make wget unzip zip python3
;;
*)
echo "Sorry, we don't recognize your distribution. Try using the docker image instead:"
echo
echo "https://docs.qmk.fm/#/getting_started_docker"
exit 1
;;
esac
;;
esac
}
install_uv() {
# Install `uv` (or update as necessary)
download_url https://astral.sh/uv/install.sh - | TMPDIR=${TMPDIR:-} UV_INSTALL_DIR=${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
}
install_qmk_cli() {
# Install the QMK CLI
uv tool install --force --with pip --upgrade --python 3.13 qmk
# QMK is installed to...
local qmk_tooldir="$(uv tool dir)/qmk"
# Convert it to a unix-style path if we're on Windows/Msys2
if [ "$(uname -o 2>/dev/null || true)" = "Msys" ]; then
qmk_tooldir="$(cygpath -u "$qmk_tooldir")"
fi
# 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 pip install --upgrade -r https://raw.githubusercontent.com/qmk/qmk_firmware/refs/heads/master/requirements.txt
uv 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=$(download_url https://api.github.com/repos/qmk/qmk_toolchains/releases/latest - | grep -oE '"tag_name": "[^"]+' | grep -oE '[^"]+$')
# Download the specific release asset with a matching keyword
local toolchain_url=$(download_url https://api.github.com/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 toolchain..." >&2
local target_file="$QMK_DISTRIB_DIR/$(basename "$toolchain_url")"
download_url "$toolchain_url" "$target_file"
# Extract the toolchain
echo "Extracting compiler toolchain to '$QMK_DISTRIB_DIR'..." >&2
tar xf "$target_file" -C "$QMK_DISTRIB_DIR" --strip-components=1
}
install_flashing_tools() {
# Get the latest flashing tools release from https://github.com/qmk/qmk_flashutils
local latest_flashutils_release=$(download_url https://api.github.com/repos/qmk/qmk_flashutils/releases/latest - | grep -oE '"tag_name": "[^"]+' | grep -oE '[^"]+$')
# Download the specific release asset with a matching keyword
local flashutils_url=$(download_url https://api.github.com/repos/qmk/qmk_flashutils/releases/tags/$latest_flashutils_release - | grep -oE '"browser_download_url": "[^"]+"' | grep -oE 'https://[^"]+' | grep $(fn_os)$(fn_arch))
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
tar xf "$target_file" -C "$QMK_DISTRIB_DIR/bin"
}
clean_tarballs() {
# Clean up the tarballs
rm -f "$QMK_DISTRIB_DIR"/*.tar.zst || true
}
# 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="$(cygpath -w "$TMP")"
export UV_INSTALL_DIR=${UV_INSTALL_DIR:-/opt/uv}
export QMK_DISTRIB_DIR=${QMK_DISTRIB_DIR:-/opt/qmk}
fi
# Work out where we want to install the distribution
export QMK_DISTRIB_DIR=${QMK_DISTRIB_DIR:-$HOME/.local/share/qmk}
script_parse_args "$@"
echo "This script will install \`uv\` to ${UV_INSTALL_DIR:-the default location}, and the QMK CLI, toolchains, and flashing utilities to ${QMK_DISTRIB_DIR}."
[ -z "${SKIP_PACKAGE_MANAGER:-}" ] || { preinstall_delay || exit 1; }
[ -n "${SKIP_PACKAGE_MANAGER:-}" ] || install_package_manager_deps
[ -n "${SKIP_UV:-}" ] || install_uv
setup_paths
[ -n "${SKIP_QMK_CLI:-}" ] || install_qmk_cli
# 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_TOOLCHAINS:-}" ] || install_toolchains
[ -n "${SKIP_QMK_FLASHUTILS:-}" ] || install_flashing_tools
clean_tarballs
# Notify the user that they may need to restart their shell to get the `qmk` command
hash -r
echo
echo "You may need to restart your shell to gain access to the 'qmk' command."
echo "Alternatively, add "$(dirname "$(command -v qmk)")" to your \$PATH:"
echo " export PATH=\"$(dirname "$(command -v qmk)"):\$PATH\""
} # this ensures the entire script is downloaded #