env-bootstrap.sh 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. #!/usr/bin/env sh
  2. # Copyright 2025 Nick Brassel (@tzarc)
  3. # SPDX-License-Identifier: GPL-2.0-or-later
  4. ################################################################################
  5. # This script will install the QMK CLI, toolchains, and flashing utilities.
  6. ################################################################################
  7. # Environment variables:
  8. # CONFIRM: Skip the pre-install delay. (or: --confirm)
  9. # QMK_DISTRIB_DIR: The directory to install the QMK distribution to. (or: --qmk-distrib-dir=...)
  10. # UV_INSTALL_DIR: The directory to install `uv` to. (or: --uv-install-dir=...)
  11. # UV_TOOL_DIR: The directory to install `uv` tools to. (or: --uv-tool-dir=...)
  12. # SKIP_CLEAN: Skip cleaning the distribution directory. (or: --skip-clean)
  13. # SKIP_PACKAGE_MANAGER: Skip installing the necessary packages for the package manager. (or: --skip-package-manager)
  14. # SKIP_UV: Skip installing `uv`. (or: --skip-uv)
  15. # SKIP_QMK_CLI: Skip installing the QMK CLI. (or: --skip-qmk-cli)
  16. # SKIP_QMK_TOOLCHAINS: Skip installing the QMK toolchains. (or: --skip-qmk-toolchains)
  17. # SKIP_QMK_FLASHUTILS: Skip installing the QMK flashing utilities. (or: --skip-qmk-flashutils)
  18. # SKIP_UDEV_RULES: Skip installing the udev rules for Linux. (or: --skip-udev-rules)
  19. # SKIP_WINDOWS_DRIVERS: Skip installing the Windows drivers for the flashing utilities. (or: --skip-windows-drivers)
  20. #
  21. # Arguments above may be negated by prefixing with `--no-` instead (e.g. `--no-skip-clean`).
  22. ################################################################################
  23. # Usage:
  24. # curl -fsSL https://raw.githubusercontent.com/qmk/qmk_firmware/master/util/env-bootstrap.sh | sh
  25. #
  26. # Help:
  27. # curl -fsSL https://raw.githubusercontent.com/qmk/qmk_firmware/master/util/env-bootstrap.sh | sh -s -- --help
  28. #
  29. # An example which skips installing `uv` using environment variables:
  30. # curl -fsSL https://raw.githubusercontent.com/qmk/qmk_firmware/master/util/env-bootstrap.sh | SKIP_UV=1 sh
  31. #
  32. # ...or by using command line arguments:
  33. # curl -fsSL https://raw.githubusercontent.com/qmk/qmk_firmware/master/util/env-bootstrap.sh | sh -s -- --skip-uv
  34. #
  35. # Any other configurable items listed above may be specified in the same way.
  36. ################################################################################
  37. { # this ensures the entire script is downloaded #
  38. set -eu
  39. # Prevent user grep settings from injecting flags (e.g. --color=always) that
  40. # corrupt captured output and break pattern matching throughout this script.
  41. unset GREP_OPTIONS GREP_COLOR GREP_COLORS
  42. # Force the C locale so `tr`/`grep`/`sed`/`sort` behave the same on every
  43. # system, and clear other variables which alter tool behavior.
  44. export LC_ALL=C
  45. unset CDPATH POSIXLY_CORRECT TAR_OPTIONS
  46. # Drop out of any inherited Python venv; `deactivate` doesn't exist in this
  47. # process, so strip its $PATH entries and marker variables by hand.
  48. if [ -n "${VIRTUAL_ENV:-}" ]; then
  49. clean_path=''
  50. old_ifs="${IFS}"
  51. IFS=':'
  52. for path_entry in $PATH; do
  53. case "$path_entry" in
  54. "$VIRTUAL_ENV"/bin | "$VIRTUAL_ENV"/Scripts) ;;
  55. *) clean_path="${clean_path:+${clean_path}:}${path_entry}" ;;
  56. esac
  57. done
  58. IFS="${old_ifs}"
  59. PATH="${clean_path}"
  60. export PATH
  61. unset VIRTUAL_ENV VIRTUAL_ENV_PROMPT clean_path old_ifs path_entry
  62. fi
  63. # Wipe all PYTHON* variables (keeping PYTHON_TARGET_VERSION, which this
  64. # script uses) so the user's Python settings can't leak into the
  65. # interpreters and builds managed by `uv`.
  66. saved_python_target_version="${PYTHON_TARGET_VERSION:-}"
  67. for env_var_name in $(env | LC_ALL=C sed -n 's/^\(PYTHON[A-Za-z0-9_]*\)=.*/\1/p'); do
  68. unset "$env_var_name" 2>/dev/null || true
  69. done
  70. [ -z "$saved_python_target_version" ] || export PYTHON_TARGET_VERSION="$saved_python_target_version"
  71. unset saved_python_target_version env_var_name
  72. export PYTHONNOUSERSITE=1
  73. # Clear conda/pip/uv overrides which would affect dependency resolution or
  74. # builds; proxy, TLS, and index/mirror variables stay for corporate networks.
  75. unset CONDA_PREFIX CONDA_DEFAULT_ENV
  76. unset PIP_REQUIRE_VIRTUALENV PIP_TARGET PIP_PREFIX PIP_USER
  77. unset SETUPTOOLS_USE_DISTUTILS
  78. unset UV_NO_BUILD_ISOLATION UV_OFFLINE UV_NO_INDEX \
  79. UV_CONSTRAINT UV_BUILD_CONSTRAINT UV_OVERRIDE \
  80. UV_SYSTEM_PYTHON UV_NO_MANAGED_PYTHON
  81. BOOTSTRAP_TMPDIR="$(mktemp -d /tmp/qmk-bootstrap-failure.XXXXXX)"
  82. trap 'rm -rf "$BOOTSTRAP_TMPDIR" >/dev/null 2>&1 || true' EXIT
  83. FAILURE_FILE="${BOOTSTRAP_TMPDIR}/fail"
  84. # Work out which `sed` to use
  85. command -v gsed >/dev/null 2>&1 && SED=gsed || SED=sed
  86. script_args() {
  87. cat <<__EOT__
  88. --help -- Shows this help text
  89. --confirm -- Skips the delay before installation
  90. --uv-install-dir={path} -- The directory to install \`uv\` into
  91. --uv-tool-dir={path} -- The directory to install \`uv\` tools into
  92. --qmk-distrib-dir={path} -- The directory to install the QMK distribution into
  93. --skip-clean -- Skip cleaning the QMK distribution directory
  94. --skip-package-manager -- Skip installing the necessary packages for the package manager
  95. --skip-uv -- Skip installing \`uv\`
  96. --skip-qmk-cli -- Skip installing the QMK CLI
  97. --skip-qmk-toolchains -- Skip installing the QMK toolchains
  98. --skip-qmk-flashutils -- Skip installing the QMK flashing utilities
  99. --skip-udev-rules -- Skip installing the udev rules for Linux
  100. --skip-windows-drivers -- Skip installing the Windows drivers for the flashing utilities
  101. __EOT__
  102. # Hidden:
  103. # --wsl-install -- Installs the WSL variant of qmk_flashutils
  104. }
  105. signal_execution_failure() {
  106. touch "$FAILURE_FILE" >/dev/null 2>&1 || true
  107. }
  108. exit_if_execution_failed() {
  109. if [ -e "$FAILURE_FILE" ]; then
  110. exit 1
  111. fi
  112. }
  113. script_help() {
  114. 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' ' ')"
  115. echo
  116. echo "Arguments:"
  117. script_args
  118. echo
  119. echo "Switch arguments may be negated by prefixing with '--no-' (e.g. '--no-skip-clean')."
  120. }
  121. script_parse_args() {
  122. local N
  123. local V
  124. while [ ! -z "${1:-}" ]; do
  125. case "$1" in
  126. --help)
  127. script_help
  128. exit 0
  129. ;;
  130. --*=*)
  131. N=${1%%=*}
  132. N=${N##--}
  133. N=$(echo $N | tr '-' '_' | tr 'a-z' 'A-Z')
  134. V=${1##*=}
  135. export $N="$V"
  136. ;;
  137. --no-*)
  138. N=${1##--no-}
  139. N=$(echo $N | tr '-' '_' | tr 'a-z' 'A-Z')
  140. unset $N
  141. ;;
  142. --*)
  143. N=${1##--}
  144. N=$(echo $N | tr '-' '_' | tr 'a-z' 'A-Z')
  145. export $N=true
  146. ;;
  147. *)
  148. echo "Unknown argument: '$1'" >&2
  149. echo
  150. script_help >&2
  151. exit 1
  152. ;;
  153. esac
  154. shift
  155. unset N
  156. unset V
  157. done
  158. }
  159. nsudo() {
  160. if [ "$(fn_os)" = "windows" ]; then
  161. # No need for sudo under QMK MSYS
  162. return
  163. elif [ $(id -u) -ne 0 ]; then
  164. if [ -n "$(command -v sudo 2>/dev/null || true)" ]; then
  165. echo "sudo"
  166. elif [ -n "$(command -v doas 2>/dev/null || true)" ]; then
  167. echo "doas"
  168. else
  169. echo "Please install 'sudo' or 'doas' to continue." >&2
  170. exit 1
  171. fi
  172. fi
  173. true
  174. }
  175. download_url() {
  176. local url=$1
  177. local filename=${2:-$(basename "$url")}
  178. local quiet=''
  179. if [ -n "$(command -v curl 2>/dev/null || true)" ]; then
  180. [ "$filename" = "-" ] && quiet='-s' || echo "Downloading '$url' => '$filename'" >&2
  181. curl -LSf $quiet -o "$filename" "$url"
  182. elif [ -n "$(command -v wget 2>/dev/null || true)" ]; then
  183. [ "$filename" = "-" ] && quiet='-q' || echo "Downloading '$url' => '$filename'" >&2
  184. wget $quiet "-O$filename" "$url"
  185. else
  186. echo "Please install 'curl' to continue." >&2
  187. exit 1
  188. fi
  189. }
  190. github_api_call() {
  191. local url="$1"
  192. local token="${GITHUB_TOKEN:-${GH_TOKEN:-}}"
  193. if [ -n "${token:-}" ]; then
  194. if [ -n "$(command -v curl 2>/dev/null || true)" ]; then
  195. curl -fsSL -H "Authorization: token $token" -H "Accept: application/vnd.github.v3+json" "https://api.github.com/$url"
  196. elif [ -n "$(command -v wget 2>/dev/null || true)" ]; then
  197. wget -q --header="Authorization: token $token" --header="Accept: application/vnd.github.v3+json" "https://api.github.com/$url" -O -
  198. fi
  199. else
  200. download_url "https://api.github.com/$url" -
  201. fi
  202. }
  203. check_release_tag() {
  204. # An empty tag means the GitHub API call failed, usually from rate limiting.
  205. if [ -z "$2" ]; then
  206. echo "Could not determine the latest $1 release." >&2
  207. echo "If GitHub API rate limits are the cause, set GITHUB_TOKEN to raise them." >&2
  208. exit 1
  209. fi
  210. }
  211. fn_os() {
  212. local os_name=$(echo ${1:-} | tr 'A-Z' 'a-z')
  213. if [ -z "$os_name" ]; then
  214. os_name=$(uname -s | tr 'A-Z' 'a-z')
  215. fi
  216. case "$os_name" in
  217. *darwin* | *macos* | *apple*)
  218. echo macos
  219. ;;
  220. *windows* | *mingw* | *msys*)
  221. echo windows
  222. ;;
  223. *linux*)
  224. echo linux
  225. ;;
  226. *)
  227. echo unknown
  228. ;;
  229. esac
  230. }
  231. fn_arch() {
  232. local arch_name=$(echo ${1:-} | tr 'A-Z' 'a-z')
  233. if [ -z "$arch_name" ]; then
  234. arch_name=$(uname -m | tr 'A-Z' 'a-z')
  235. fi
  236. case "$arch_name" in
  237. *arm64* | *aarch64*)
  238. echo ARM64
  239. ;;
  240. *riscv64*)
  241. echo RV64
  242. ;;
  243. *x86_64* | *x64*)
  244. echo X64
  245. ;;
  246. *)
  247. echo unknown
  248. ;;
  249. esac
  250. }
  251. preinstall_delay() {
  252. [ -z "${CONFIRM:-}" ] || return 0
  253. echo >&2
  254. echo "Waiting 10 seconds before proceeding. Press Ctrl+C to cancel installation." >&2
  255. sleep 10
  256. }
  257. get_package_manager_deps() {
  258. case $(fn_os) in
  259. macos) echo "zstd clang-format make hidapi libusb dos2unix git" ;;
  260. windows) echo "base-devel: zstd:p toolchain:p clang:p hidapi:p dos2unix: git: unzip:" ;;
  261. linux)
  262. if ldd --version 2>&1 | grep -qi musl; then
  263. echo >&2
  264. echo "Sorry, QMK's pre-built toolchains are compiled against glibc and will not run on musl-based Linux distributions." >&2
  265. echo >&2
  266. echo "Try using a glibc-based distribution, or use Docker instead:" >&2
  267. echo " - https://docs.qmk.fm/newbs_getting_started#set-up-your-environment" >&2
  268. echo " - https://docs.qmk.fm/#/getting_started_docker" >&2
  269. echo >&2
  270. echo "If you cannot use a compatible distro, you can try installing the \`qmk\` Python package manually using \`pip\`, most likely requiring a virtual environment:" >&2
  271. echo " % python3 -m pip install qmk" >&2
  272. echo >&2
  273. echo "All other dependencies will need to be installed manually, such as make, git, AVR and ARM toolchains, and associated flashing utilities." >&2
  274. echo >&2
  275. echo "**NOTE**: QMK does not provide official support for musl-based environments. Here be dragons, you are on your own." >&2
  276. signal_execution_failure
  277. return
  278. fi
  279. case $(grep ID /etc/os-release) in
  280. *arch* | *manjaro* | *cachyos*) echo "zstd base-devel clang diffutils wget unzip zip hidapi dos2unix git" ;;
  281. *debian* | *ubuntu*) echo "zstd build-essential clang-format diffutils wget unzip zip libhidapi-hidraw0 dos2unix git" ;;
  282. *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" ;;
  283. *suse*) echo "zstd make gcc binutils clang diffutils wget unzip zip libhidapi-hidraw0 dos2unix git libusb-1_0-devel gzip which" ;;
  284. *gentoo*) echo "zstd sys-apps/diffutils wget unzip zip dev-libs/hidapi dos2unix dev-vcs/git dev-libs/libusb app-arch/gzip which" ;;
  285. *)
  286. echo >&2
  287. echo "Sorry, we don't recognize your distribution." >&2
  288. echo >&2
  289. echo "Proceeding with the installation, however you will need to install at least the following tools manually:" >&2
  290. echo " - make, git, curl, zstd, unzip, [lib]hidapi" >&2
  291. echo "Other tools may be required depending on your distribution." >&2
  292. echo >&2
  293. echo "Alternatively, if you prefer Docker, try using the docker image instead:" >&2
  294. echo " - https://docs.qmk.fm/#/getting_started_docker" >&2
  295. ;;
  296. esac
  297. ;;
  298. *)
  299. # We can only really support macOS, Windows, and Linux at this time due to `uv` requirements.
  300. echo >&2
  301. echo "Sorry, we don't recognize your OS. Try using a compatible OS instead:" >&2
  302. echo " - https://docs.qmk.fm/newbs_getting_started#set-up-your-environment" >&2
  303. echo >&2
  304. 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
  305. echo " % python3 -m pip install qmk" >&2
  306. echo >&2
  307. echo "All other dependencies will need to be installed manually, such as make, git, AVR and ARM toolchains, and associated flashing utilities." >&2
  308. echo >&2
  309. echo "**NOTE**: QMK does not provide official support for your environment. Here be dragons, you are on your own." >&2
  310. signal_execution_failure
  311. ;;
  312. esac
  313. }
  314. print_package_manager_deps_and_delay() {
  315. get_package_manager_deps | tr ' ' '\n' | sort | xargs -I'{}' echo " - {}" >&2
  316. exit_if_execution_failed
  317. if [ -n "${1:-}" ]; then
  318. echo >&2
  319. echo "$1" >&2
  320. fi
  321. preinstall_delay || exit 1
  322. }
  323. install_package_manager_deps() {
  324. # Install the necessary packages for the package manager
  325. case $(fn_os) in
  326. macos)
  327. if [ -n "$(command -v brew 2>/dev/null || true)" ]; then
  328. echo "It will also install the following system packages using 'brew':" >&2
  329. local intel_note=""
  330. if [ "$(fn_arch)" = "X64" ]; then
  331. intel_note="NOTE: Homebrew no longer provides pre-built packages for Intel Macs, so some of the above may be built from source. This can take a long time."
  332. fi
  333. print_package_manager_deps_and_delay "$intel_note"
  334. brew update
  335. local existing=""
  336. local new=""
  337. for dep in $(get_package_manager_deps); do
  338. if brew list --formula | grep -q "^${dep}\$"; then
  339. existing="${existing:-} $dep"
  340. else
  341. new="${new:-} $dep"
  342. fi
  343. done
  344. # Homebrew no longer builds Intel macOS bottles (tier 3); when a
  345. # bottle is missing, retry the formula as a source build.
  346. for dep in ${existing:-}; do
  347. brew upgrade "$dep" || brew upgrade --build-from-source "$dep"
  348. done
  349. for dep in ${new:-}; do
  350. brew install "$dep" || brew install --build-from-source "$dep"
  351. done
  352. else
  353. echo "Please install 'brew' to continue. See https://brew.sh/ for more information." >&2
  354. exit 1
  355. fi
  356. ;;
  357. windows)
  358. echo "It will also install the following packages using 'pacman'/'pacboy':" >&2
  359. print_package_manager_deps_and_delay
  360. $(nsudo) pacman --needed --noconfirm --disable-download-timeout -S pactoys
  361. $(nsudo) pacboy sync --needed --noconfirm --disable-download-timeout $(get_package_manager_deps)
  362. ;;
  363. linux)
  364. case $(grep ID /etc/os-release) in
  365. *arch* | *manjaro* | *cachyos*)
  366. echo "It will also install the following system packages using 'pacman':" >&2
  367. print_package_manager_deps_and_delay
  368. $(nsudo) pacman --needed --noconfirm -S $(get_package_manager_deps)
  369. ;;
  370. *debian* | *ubuntu*)
  371. echo "It will also install the following system packages using 'apt':" >&2
  372. print_package_manager_deps_and_delay
  373. $(nsudo) apt-get update
  374. DEBIAN_FRONTEND=noninteractive \
  375. $(nsudo) apt-get --quiet --yes install $(get_package_manager_deps)
  376. ;;
  377. *fedora*)
  378. echo "It will also install the following system packages using 'dnf':" >&2
  379. print_package_manager_deps_and_delay
  380. # Some RHEL-likes need EPEL for hidapi and libusb packages
  381. $(nsudo) dnf -y install epel-release 2>/dev/null || true
  382. # RHEL-likes have naming differences in libusb/hidapi packages; try each individually
  383. $(nsudo) dnf -y install $(get_package_manager_deps | tr ' ' '\n' | grep -v 'epel-release' | grep -v libusb | grep -v hidapi | tr '\n' ' ')
  384. for pkg in $(get_package_manager_deps | tr ' ' '\n' | grep -E 'libusb|hidapi'); do
  385. $(nsudo) dnf -y install "$pkg" 2>/dev/null || true
  386. done
  387. ;;
  388. *opensuse* | *suse*)
  389. echo "It will also install the following system packages using 'zypper':" >&2
  390. print_package_manager_deps_and_delay
  391. $(nsudo) zypper --non-interactive refresh
  392. $(nsudo) zypper --non-interactive install $(get_package_manager_deps)
  393. ;;
  394. *gentoo*)
  395. echo "It will also install the following system packages using 'emerge':" >&2
  396. print_package_manager_deps_and_delay
  397. $(nsudo) emaint sync
  398. $(nsudo) emerge --noreplace --ask=n $(get_package_manager_deps | tr ' ' '\n') || signal_execution_failure
  399. exit_if_execution_failed
  400. ;;
  401. *)
  402. print_package_manager_deps_and_delay
  403. echo "Proceeding with the installation, you will need to ensure prerequisites are installed." >&2
  404. ;;
  405. esac
  406. ;;
  407. *)
  408. print_package_manager_deps_and_delay
  409. ;;
  410. esac
  411. }
  412. install_uv() {
  413. # Install `uv` (or update as necessary)
  414. download_url https://astral.sh/uv/install.sh - | TMPDIR="$(posix_ish_path "${TMPDIR:-}")" UV_INSTALL_DIR="$(windows_ish_path "${UV_INSTALL_DIR:-}")" sh
  415. # Workaround for UV installer not pushing UV_TOOL_BIN_DIR into the env when used with their installer
  416. if [ "$(uname -o 2>/dev/null || true)" = "Msys" ]; then
  417. if [ "${UV_NO_MODIFY_PATH:-}" != "1" ]; then
  418. echo -e "#!/bin/sh\n# Workaround for UV installer not pushing UV_TOOL_BIN_DIR into the env when used with their installer\nexport PATH=\"${UV_TOOL_BIN_DIR}:\$PATH\"" > /etc/profile.d/qmk-uv-env.sh
  419. fi
  420. fi
  421. }
  422. setup_paths() {
  423. # Set up the paths for any of the locations `uv` expects
  424. if [ -n "${XDG_BIN_HOME:-}" ]; then
  425. export PATH="$XDG_BIN_HOME:$PATH"
  426. fi
  427. if [ -n "${XDG_DATA_HOME:-}" ]; then
  428. export PATH="$XDG_DATA_HOME/../bin:$PATH"
  429. fi
  430. [ ! -d "$HOME/.local/bin" ] || export PATH="$HOME/.local/bin:$PATH"
  431. if [ -n "${UV_INSTALL_DIR:-}" ]; then
  432. export PATH="$UV_INSTALL_DIR/bin:$UV_INSTALL_DIR:$PATH" # cater for both "flat" and "hierarchical" installs of `uv`
  433. fi
  434. if [ -n "${UV_TOOL_BIN_DIR:-}" ]; then
  435. export PATH="$UV_TOOL_BIN_DIR:$PATH"
  436. fi
  437. }
  438. uv_command() {
  439. if [ "$(fn_os)" = "windows" ]; then
  440. UV_TOOL_DIR="$(windows_ish_path "${UV_TOOL_DIR:-}")" \
  441. UV_TOOL_BIN_DIR="$(windows_ish_path "${UV_TOOL_BIN_DIR:-}")" \
  442. uv "$@"
  443. else
  444. uv "$@"
  445. fi
  446. }
  447. install_qmk_cli() {
  448. # Install the QMK CLI
  449. uv_command tool install --force --with pip --upgrade --python $PYTHON_TARGET_VERSION qmk
  450. # QMK is installed to...
  451. local qmk_tooldir="$(posix_ish_path "$(uv_command tool dir)/qmk")"
  452. # Activate the environment
  453. if [ -e "$qmk_tooldir/bin" ]; then
  454. . "$qmk_tooldir/bin/activate"
  455. elif [ -e "$qmk_tooldir/Scripts" ]; then
  456. . "$qmk_tooldir/Scripts/activate"
  457. else
  458. echo "Could not find the QMK environment to activate." >&2
  459. exit 1
  460. fi
  461. # Install the QMK dependencies
  462. uv_command pip install --upgrade -r https://raw.githubusercontent.com/qmk/qmk_firmware/refs/heads/master/requirements.txt
  463. uv_command pip install --upgrade -r https://raw.githubusercontent.com/qmk/qmk_firmware/refs/heads/master/requirements-dev.txt
  464. # Deactivate the environment
  465. deactivate
  466. }
  467. install_toolchains() {
  468. # Get the latest toolchain release from https://github.com/qmk/qmk_toolchains
  469. local latest_toolchains_release=$(github_api_call repos/qmk/qmk_toolchains/releases/latest - | grep -oE '"tag_name": "[^"]+' | grep -oE '[^"]+$')
  470. check_release_tag qmk_toolchains "$latest_toolchains_release"
  471. # Download the specific release asset with a matching keyword
  472. local toolchain_url=$(github_api_call repos/qmk/qmk_toolchains/releases/tags/$latest_toolchains_release - | grep -oE '"browser_download_url": "[^"]+"' | grep -oE 'https://[^"]+' | grep -E "qmk_toolchains-.*$(fn_os)$(fn_arch)")
  473. if [ -z "$toolchain_url" ]; then
  474. echo "No toolchain found for this OS/Arch combination." >&2
  475. exit 1
  476. fi
  477. # Download the toolchain release to the toolchains location
  478. echo "Downloading compiler toolchains..." >&2
  479. local target_file="$QMK_DISTRIB_DIR/$(basename "$toolchain_url")"
  480. download_url "$toolchain_url" "$target_file"
  481. # Extract the toolchain
  482. echo "Extracting compiler toolchains to '$QMK_DISTRIB_DIR'..." >&2
  483. zstdcat "$target_file" | tar xf - -C "$QMK_DISTRIB_DIR" --strip-components=1
  484. }
  485. install_flashing_tools() {
  486. local osarchvariant="$(fn_os)$(fn_arch)"
  487. # Special case for WSL
  488. if [ -n "${WSL_INSTALL:-}" ] || [ -n "${WSL_DISTRO_NAME:-}" ] || [ -f /proc/sys/fs/binfmt_misc/WSLInterop ]; then
  489. osarchvariant="windowsWSL"
  490. fi
  491. # Get the latest flashing tools release from https://github.com/qmk/qmk_flashutils
  492. local latest_flashutils_release=$(github_api_call repos/qmk/qmk_flashutils/releases/latest - | grep -oE '"tag_name": "[^"]+' | grep -oE '[^"]+$')
  493. check_release_tag qmk_flashutils "$latest_flashutils_release"
  494. # Download the specific release asset with a matching keyword
  495. local flashutils_url=$(github_api_call repos/qmk/qmk_flashutils/releases/tags/$latest_flashutils_release - | grep -oE '"browser_download_url": "[^"]+"' | grep -oE 'https://[^"]+' | grep -E "qmk_flashutils-.*$osarchvariant")
  496. if [ -z "$flashutils_url" ]; then
  497. echo "No flashing tools found for this OS/Arch combination." >&2
  498. exit 1
  499. fi
  500. # Download the flashing tools release to the toolchains location
  501. echo "Downloading flashing tools..." >&2
  502. local target_file="$QMK_DISTRIB_DIR/$(basename "$flashutils_url")"
  503. download_url "$flashutils_url" "$target_file"
  504. # Extract the flashing tools
  505. echo "Extracting flashing tools to '$QMK_DISTRIB_DIR'..." >&2
  506. zstdcat "$target_file" | tar xf - -C "$QMK_DISTRIB_DIR/bin"
  507. # Move the release file to etc
  508. mv "$QMK_DISTRIB_DIR/bin/flashutils_release"* "$QMK_DISTRIB_DIR/etc"
  509. }
  510. install_linux_udev_rules() {
  511. # Get the latest qmk_udev release
  512. local latest_udev_release=$(github_api_call repos/qmk/qmk_udev/releases/latest - | grep -oE '"tag_name": "[^"]+' | grep -oE '[^"]+$')
  513. check_release_tag qmk_udev "$latest_udev_release"
  514. echo "Using qmk_udev release: $latest_udev_release" >&2
  515. # Download the udev rules file
  516. local qmk_rules_file="$QMK_DISTRIB_DIR/50-qmk.rules"
  517. local release_base="https://github.com/qmk/qmk_udev/releases/download/$latest_udev_release"
  518. download_url "$release_base/50-qmk.rules" "$qmk_rules_file"
  519. # Download the architecture-appropriate qmk_id binary
  520. local arch="$(fn_arch)"
  521. local qmk_id_file="$QMK_DISTRIB_DIR/qmk_id"
  522. download_url "$release_base/qmk_id-linux${arch}" "$qmk_id_file"
  523. # Remove existing QMK udev rules and qmk_id helpers from all standard locations
  524. echo "Removing existing QMK udev rules and helpers..." >&2
  525. for dir in /etc/udev/rules.d /run/udev/rules.d /usr/lib/udev/rules.d /usr/local/lib/udev/rules.d /lib/udev/rules.d; do
  526. if [ -d "$dir" ]; then
  527. for f in "$dir"/*-qmk.rules; do
  528. [ -e "$f" ] && echo "Removing $f" >&2 && $(nsudo) rm -f "$f"
  529. done
  530. fi
  531. done
  532. for dir in /usr/lib/udev /usr/local/lib/udev /lib/udev; do
  533. [ -e "$dir/qmk_id" ] && echo "Removing $dir/qmk_id" >&2 && $(nsudo) rm -f "$dir/qmk_id"
  534. done
  535. # Install qmk_id binary
  536. echo "Installing /usr/lib/udev/qmk_id ..." >&2
  537. $(nsudo) install -d -m 0755 /usr/lib/udev
  538. $(nsudo) install -m 0755 "$qmk_id_file" /usr/lib/udev/qmk_id
  539. # Install udev rules
  540. echo "Installing /etc/udev/rules.d/50-qmk.rules ..." >&2
  541. $(nsudo) install -d -m 0755 /etc/udev/rules.d
  542. $(nsudo) install -m 0644 "$qmk_rules_file" /etc/udev/rules.d/50-qmk.rules
  543. # Clean up downloaded files
  544. rm -f "$qmk_rules_file" "$qmk_id_file" || true
  545. # Reload udev rules
  546. if command -v udevadm >/dev/null 2>&1; then
  547. echo "Reloading udev rules..." >&2
  548. $(nsudo) udevadm control --reload-rules || true
  549. $(nsudo) udevadm trigger || true
  550. else
  551. echo "udevadm not found, skipping udev rules reload." >&2
  552. fi
  553. }
  554. install_windows_drivers() {
  555. # Get the latest driver installer release from https://github.com/qmk/qmk_driver_installer
  556. local latest_driver_installer_release=$(github_api_call repos/qmk/qmk_driver_installer/releases/latest - | grep -oE '"tag_name": "[^"]+' | grep -oE '[^"]+$')
  557. check_release_tag qmk_driver_installer "$latest_driver_installer_release"
  558. # Download the specific release asset
  559. 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')
  560. if [ -z "$driver_installer_url" ]; then
  561. echo "No driver installer found." >&2
  562. exit 1
  563. fi
  564. # Download the driver installer release to the toolchains location
  565. echo "Downloading driver installer..." >&2
  566. local target_file="$QMK_DISTRIB_DIR/$(basename "$driver_installer_url")"
  567. download_url "$driver_installer_url" "$target_file"
  568. # Download the drivers list
  569. download_url "https://raw.githubusercontent.com/qmk/qmk_firmware/refs/heads/master/util/drivers.txt" "$QMK_DISTRIB_DIR/drivers.txt"
  570. # Execute the driver installer
  571. cd "$QMK_DISTRIB_DIR"
  572. cmd.exe //c "qmk_driver_installer.exe --all --force drivers.txt"
  573. cd -
  574. # Remove the temporary files
  575. rm -f "$QMK_DISTRIB_DIR/qmk_driver_installer.exe" "$QMK_DISTRIB_DIR/drivers.txt" || true
  576. }
  577. clean_tarballs() {
  578. # Clean up the tarballs
  579. rm -f "$QMK_DISTRIB_DIR"/*.tar.zst || true
  580. }
  581. windows_ish_path() {
  582. [ -n "$1" ] || return 0
  583. [ "$(uname -o 2>/dev/null || true)" = "Msys" ] && cygpath -w "$1" || echo "$1"
  584. }
  585. posix_ish_path() {
  586. [ -n "$1" ] || return 0
  587. [ "$(uname -o 2>/dev/null || true)" = "Msys" ] && cygpath -u "$1" || echo "$1"
  588. }
  589. # Set the Python version we want to use with the QMK CLI
  590. export PYTHON_TARGET_VERSION=${PYTHON_TARGET_VERSION:-3.14}
  591. # Windows/MSYS doesn't like `/tmp` so we need to set a different temporary directory.
  592. # 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.
  593. if [ "$(uname -o 2>/dev/null || true)" = "Msys" ]; then
  594. export TMPDIR="$(posix_ish_path "$TMP")"
  595. export UV_INSTALL_DIR="$(posix_ish_path "${UV_INSTALL_DIR:-/opt/uv}")"
  596. export QMK_DISTRIB_DIR="$(posix_ish_path "${QMK_DISTRIB_DIR:-/opt/qmk}")"
  597. export UV_TOOL_DIR="$(posix_ish_path "${UV_TOOL_DIR:-"$UV_INSTALL_DIR/tools"}")"
  598. export UV_TOOL_BIN_DIR="$(posix_ish_path "$UV_TOOL_DIR/bin")"
  599. fi
  600. script_parse_args "$@"
  601. echo "This QMK CLI installation script will install \`uv\`, the QMK CLI, as well as QMK-supplied toolchains and flashing utilities." >&2
  602. [ -z "${SKIP_PACKAGE_MANAGER:-}" ] || { preinstall_delay || exit 1; }
  603. [ -n "${SKIP_PACKAGE_MANAGER:-}" ] || install_package_manager_deps
  604. [ -n "${SKIP_UV:-}" ] || install_uv
  605. # Make sure the usual `uv` and other associated directories are on the $PATH
  606. setup_paths
  607. # Work out where we want to install the distribution and tools now that `uv` is installed
  608. export QMK_DISTRIB_DIR="$(posix_ish_path "${QMK_DISTRIB_DIR:-$(printf 'import platformdirs\nprint(platformdirs.user_data_dir("qmk"))' | uv_command run --quiet --no-project --python $PYTHON_TARGET_VERSION --with platformdirs -)}")"
  609. # `export` masks any failure of the `uv` invocation above, so bail out here
  610. # rather than continue with an empty directory.
  611. if [ -z "$QMK_DISTRIB_DIR" ]; then
  612. echo "Could not determine the QMK distribution directory." >&2
  613. exit 1
  614. fi
  615. # Clear out the distrib directory if necessary
  616. if [ -z "${SKIP_CLEAN:-}" ] || [ -z "${SKIP_QMK_TOOLCHAINS:-}" -a -z "${SKIP_QMK_FLASHUTILS:-}" ]; then
  617. if [ -d "$QMK_DISTRIB_DIR" ]; then
  618. echo "Removing old QMK distribution..." >&2
  619. rm -rf "$QMK_DISTRIB_DIR"
  620. fi
  621. fi
  622. mkdir -p "$QMK_DISTRIB_DIR"
  623. [ -n "${SKIP_QMK_CLI:-}" ] || install_qmk_cli
  624. [ -n "${SKIP_QMK_TOOLCHAINS:-}" ] || install_toolchains
  625. [ -n "${SKIP_QMK_FLASHUTILS:-}" ] || install_flashing_tools
  626. if [ "$(uname -s 2>/dev/null || true)" = "Linux" ]; then
  627. [ -n "${SKIP_UDEV_RULES:-}" ] || install_linux_udev_rules
  628. fi
  629. if [ "$(uname -o 2>/dev/null || true)" = "Msys" ]; then
  630. [ -n "${SKIP_WINDOWS_DRIVERS:-}" ] || install_windows_drivers
  631. fi
  632. clean_tarballs
  633. # Notify the user that they may need to restart their shell to get the `qmk` command
  634. echo >&2
  635. echo "QMK CLI installation complete." >&2
  636. echo "The QMK CLI has been installed to '$(posix_ish_path "$(dirname "$(command -v qmk)")")'." >&2
  637. echo "The QMK CLI venv has been created at '$(posix_ish_path "$(uv_command tool dir)/qmk")'." >&2
  638. echo "Toolchains and flashing utilities have been installed to '$QMK_DISTRIB_DIR'." >&2
  639. echo >&2
  640. echo "You may need to restart your shell to gain access to the 'qmk' command." >&2
  641. echo "Alternatively, add "$(posix_ish_path "$(dirname "$(command -v qmk)")")" to your \$PATH:" >&2
  642. echo " export PATH=\"$(posix_ish_path "$(dirname "$(command -v qmk)")"):\$PATH\"" >&2
  643. } # this ensures the entire script is downloaded #