Compare commits

...

27 Commits

Author SHA1 Message Date
zvecr 2de8cf606c Convert to f-strings 2026-04-22 23:13:12 +01:00
Joel Challis 1f6bb7dab0 Fix badge link in README.md 2026-04-21 00:45:36 +01:00
Joel Challis 2eb3279945 Align README with other repos 2026-04-21 00:41:11 +01:00
Joel Challis 8fd7af98d9 Remove qmk_fpm references (#233) 2026-04-21 00:21:40 +01:00
Joel Challis 240ccd781a Remove the release script 2026-04-21 00:18:36 +01:00
Joel Challis af9e8063f6 Remove Windows environment checks (#232) 2026-04-20 18:18:03 +01:00
dependabot[bot] 3054dad82b Bump docker/setup-buildx-action from 3 to 4 (#226)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 20:45:31 +00:00
dependabot[bot] 39a271132b Bump docker/build-push-action from 6.18.0 to 7.0.0 (#227)
* Bump docker/build-push-action from 6.18.0 to 7.0.0

Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.18.0 to 7.0.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.18.0...v7.0.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Apply suggestions from code review

Co-authored-by: Joel Challis <git@zvecr.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Joel Challis <git@zvecr.com>
2026-03-13 20:45:16 +00:00
dependabot[bot] c19c21449e Bump docker/login-action from 3 to 4 (#225)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 20:44:55 +00:00
dependabot[bot] f245cb7899 Bump docker/setup-qemu-action from 3 to 4 (#224)
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 20:44:19 +00:00
dependabot[bot] 0b5866159a Bump WyriHaximus/github-action-get-previous-tag from 1 to 2 (#221)
Bumps [WyriHaximus/github-action-get-previous-tag](https://github.com/wyrihaximus/github-action-get-previous-tag) from 1 to 2.
- [Release notes](https://github.com/wyrihaximus/github-action-get-previous-tag/releases)
- [Commits](https://github.com/wyrihaximus/github-action-get-previous-tag/compare/v1...v2)

---
updated-dependencies:
- dependency-name: WyriHaximus/github-action-get-previous-tag
  dependency-version: '2'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 20:44:01 +00:00
QMK Bot 0dd2280781 New release: 1.1.8 → 1.2.0 2025-11-27 12:59:23 +00:00
Nick Brassel c049da0c6f Align QMK distrib search with qmk_firmware. (#219) 2025-11-27 23:35:47 +11:00
Joel Challis 7b69bd6f11 Align published container tags (#218) 2025-11-27 22:37:12 +11:00
Joel Challis c594e5df09 Build Dockerfile for PRs (#217) 2025-11-27 22:36:57 +11:00
Joel Challis 915bb40473 Align order of userspace search (#205) 2025-11-27 22:36:42 +11:00
Joel Challis af1403fc41 Fix relative paths if qmk_firmware exists elsewhere (#206) 2025-11-27 22:36:27 +11:00
Nick Brassel 13e5f5c855 Global install of QMK bootstrap distribution. (#216) 2025-11-27 07:07:38 +00:00
Nick Brassel f393ad0053 Use bootstrapper for build environment installation. (#210)
* Use bootstrapper for build environment installation.

* Publishing tags.

* Path.

* Minimisation of final image.

* Minimisation of final image.

* QMK distribution $PATH manipulation, to match qmk_firmware.

* QMK distribution $PATH manipulation, to match qmk_firmware.

* Fixup library search path on macOS.

* Escape hatch for path prefix.

* Add new environment variables.
2025-11-26 07:28:03 +11:00
dependabot[bot] 947e49a15d Bump actions/checkout from 5 to 6 (#214)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-25 19:51:30 +00:00
dependabot[bot] cd3ed07cd0 Bump github/codeql-action from 3 to 4 (#213)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-16 00:35:07 +01:00
dependabot[bot] 558e248e20 Bump actions/setup-python from 5 to 6 (#212)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-15 22:56:37 +01:00
dependabot[bot] cd1e98e9d0 Bump actions/checkout from 4 to 5 (#211)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-15 22:54:34 +01:00
dependabot[bot] 8c2ae297ed Bump docker/build-push-action from 6.16.0 to 6.18.0 (#208)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.16.0 to 6.18.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.16.0...v6.18.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 6.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-11 19:20:56 +01:00
QMK Bot 7be310f364 New release: 1.1.7 → 1.1.8 2025-05-28 13:14:51 +00:00
Nick Brassel 2316a34ce9 Hope this works. (#209) 2025-05-28 23:13:37 +10:00
dependabot[bot] 36c23e3de6 Bump docker/build-push-action from 6.15.0 to 6.16.0 (#204)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.15.0 to 6.16.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.15.0...v6.16.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 6.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-27 00:33:51 +01:00
20 changed files with 269 additions and 209 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 1.1.7
current_version = 1.2.0
commit = True
tag = True
tag_name = {new_version}
+5 -8
View File
@@ -15,7 +15,7 @@ jobs:
QMK_HOME: ~/qmk_firmware
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install dependencies
run: apt-get update && apt-get install -y python3-venv
@@ -32,16 +32,13 @@ jobs:
strategy:
matrix:
os: [macos-latest, ubuntu-latest]
python-version: ['3.9', '3.10', '3.11', '3.12']
include:
- os: macos-13
python-version: '3.9'
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
@@ -56,7 +53,7 @@ jobs:
QMK_HOME: $HOME/qmk_firmware
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: (MSYS2) Setup and install dependencies
uses: msys2/setup-msys2@v2
+4 -4
View File
@@ -39,11 +39,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -54,7 +54,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@v4
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -68,4 +68,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
+42
View File
@@ -0,0 +1,42 @@
name: Test Docker Image
on:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.9'
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- uses: actions/checkout@v6
- name: Run ci_tests
run: ./ci_tests
- name: Install dependencies
run: |
python3 -m pip install --upgrade pip
pip install setuptools wheel
pip install -r requirements-dev.txt
- name: Build Python
run: |
python3 -m build
- name: Build Container
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64
+12 -8
View File
@@ -10,43 +10,43 @@ jobs:
steps:
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.9'
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: 'Get Previous tag'
id: previoustag
uses: "WyriHaximus/github-action-get-previous-tag@v1"
uses: "WyriHaximus/github-action-get-previous-tag@v2"
- name: Download previous artifact
run: |
pip download -d "${GITHUB_WORKSPACE}/dist" qmk=="${{ steps.previoustag.outputs.tag }}"
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v6.15.0
uses: docker/build-push-action@v7
with:
context: .
push: true
@@ -54,3 +54,7 @@ jobs:
tags: |
ghcr.io/qmk/qmk_cli:latest
qmkfm/qmk_cli:latest
ghcr.io/qmk/qmk_cli:${{ github.sha }}
qmkfm/qmk_cli:${{ github.sha }}
ghcr.io/qmk/qmk_cli:${{ steps.previoustag.outputs.tag }}
qmkfm/qmk_cli:${{ steps.previoustag.outputs.tag }}
+12 -11
View File
@@ -18,17 +18,17 @@ jobs:
steps:
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.9'
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Run ci_tests
run: ./ci_tests
@@ -43,6 +43,7 @@ jobs:
run: |
git config --local user.email "hello@qmk.fm"
git config --local user.name "QMK Bot"
bumpversion --dry-run --list ${{ github.event.inputs.version_part }} | grep new_version= | sed "s/new_version/NEW_CLI_VERSION/" >> "$GITHUB_ENV"
bumpversion ${{ github.event.inputs.version_part }}
- name: Push changes
@@ -61,20 +62,20 @@ jobs:
twine upload dist/*
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Build and Push to Docker Hub
uses: docker/build-push-action@v6.15.0
uses: docker/build-push-action@v7
with:
context: .
push: true
@@ -82,8 +83,8 @@ jobs:
tags: |
ghcr.io/qmk/qmk_cli:latest
qmkfm/qmk_cli:latest
ghcr.io/qmk/qmk_cli:${{ github.sha }}
qmkfm/qmk_cli:${{ github.sha }}
ghcr.io/qmk/qmk_cli:${{ env.NEW_CLI_VERSION }}
qmkfm/qmk_cli:${{ env.NEW_CLI_VERSION }}
- name: Trigger OS package builds
run: ./trigger_packages
env:
QMK_BOT_TOKEN: ${{ secrets.QMK_BOT_TOKEN }}
+27 -5
View File
@@ -1,13 +1,35 @@
FROM ghcr.io/qmk/qmk_base_container:latest
FROM ghcr.io/qmk/qmk_base_container:latest as builder
# Copy package in
ADD dist /tmp/dist
# Install QMK CLI via bootstrap script
RUN mkdir -p /opt/uv/bin \
&& export UV_PYTHON_INSTALL_DIR=/opt/uv/bin \
&& /bin/bash -c "curl -fsSL https://install.qmk.fm | sh -s -- --confirm --uv-install-dir=/usr/local/bin --uv-tool-dir=/opt/uv/tools --qmk-distrib-dir=/opt/qmk"
# Do the equivalent of entering the virtual environment
ENV PATH=/opt/qmk/bin:/opt/uv/tools/qmk/bin:/usr/local/bin:$PATH \
VIRTUAL_ENV=/opt/uv/tools/qmk
# Install python packages
RUN python3 -m pip uninstall -y qmk || true
RUN python3 -m pip install --upgrade pip setuptools wheel nose2 && \
python3 -m pip install /tmp/dist/qmk-*.whl && \
rm -rf /tmp/dist
python3 -m pip install /tmp/dist/qmk-*.whl
# Allow the uv-installed python to be usable by others
RUN chmod -R go=u,go-w /opt/uv /opt/qmk
# 2nd stage so we don't have /tmp/dist in the final image
FROM ghcr.io/qmk/qmk_base_container:latest
COPY --from=builder /opt/uv /opt/uv
COPY --from=builder /opt/qmk /opt/qmk
# Do the equivalent of entering the virtual environment
ENV PATH=/opt/qmk/bin:/opt/uv/tools/qmk/bin:$PATH \
VIRTUAL_ENV=/opt/uv/tools/qmk \
QMK_DISTRIB_DIR=/opt/qmk \
QMK_HOME=/qmk_firmware \
QMK_USERSPACE=/qmk_userspace
# Set the default location for qmk_firmware
ENV QMK_HOME /qmk_firmware
+15 -17
View File
@@ -1,9 +1,12 @@
# QMK CLI
[![CLI Setup](https://github.com/qmk/qmk_cli/actions/workflows/cli_setup.yml/badge.svg)](https://github.com/qmk/qmk_cli/actions/workflows/cli_setup.yml)
A program to help users work with [QMK Firmware](https://qmk.fm/).
> A program to help users work with [QMK Firmware](https://qmk.fm/).
# Features
[![Latest Release](https://img.shields.io/github/v/tag/qmk/qmk_cli?color=3D87CE&label=Latest&sort=semver&style=for-the-badge)](https://github.com/qmk/qmk_cli/tags)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/qmk/qmk_cli/cli_setup.yml?logo=github&style=for-the-badge)](https://github.com/qmk/qmk_cli/actions?query=workflow%3ACLI+branch%3Amaster)
[![Discord](https://img.shields.io/discord/440868230475677696.svg?logo=discord&logoColor=white&color=7289DA&style=for-the-badge)](https://discord.gg/qmk)
## Features
* Interact with your qmk_firmware tree from any location
* Use `qmk clone` to pull down anyone's `qmk_firmware` fork
@@ -17,30 +20,25 @@ A program to help users work with [QMK Firmware](https://qmk.fm/).
* `qmk lint`
* ...and many more!
# Packages
## Quickstart
Follow our [documentation to get started](https://docs.qmk.fm/newbs_getting_started).
## Packages
We provide "install and go" packages for many Operating Systems.
## Linux
Packages for several distributions available here: https://github.com/qmk/qmk_fpm
## macOS
### macOS
Using [Homebrew](https://brew.sh):
brew install qmk/qmk/qmk
## Windows
### Windows
Download our custom MSYS2 installer here: https://msys.qmk.fm/
# Quickstart
* `python3 -m pip install qmk`
* `qmk setup`
# Building
## Building
We follow PEP517, you can install using [build](https://pypi.org/project/build/):
@@ -54,6 +52,6 @@ Build:
You can read more about working with PEP517 packages in the [Python Packaging User Guide](https://packaging.python.org/guides/distributing-packages-using-setuptools/#packaging-your-project).
# Documentation
## Documentation
Full documentation: <https://docs.qmk.fm/#/tutorial>
+1 -1
View File
@@ -1,3 +1,3 @@
"""A program to help you work with qmk_firmware."""
__version__ = '1.1.7'
__version__ = '1.2.0'
+7 -12
View File
@@ -4,10 +4,6 @@ import subprocess
from milc import cli
default_repo = 'qmk_firmware'
default_fork = 'qmk/' + default_repo
default_branch = 'master'
def git_clone(url, destination, branch):
git_clone = [
@@ -18,7 +14,7 @@ def git_clone(url, destination, branch):
url,
str(destination),
]
cli.log.debug('Git clone command: %s', git_clone)
cli.log.debug(f'Git clone command: {git_clone}', )
try:
with subprocess.Popen(git_clone, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True, encoding='utf-8') as p:
@@ -28,13 +24,12 @@ def git_clone(url, destination, branch):
except Exception as e:
git_cmd = ' '.join([s.replace(' ', r'\ ') for s in git_clone])
cli.log.error("Could not run '%s': %s: %s", git_cmd, e.__class__.__name__, e)
cli.log.error(f'Could not run "{git_cmd}": {e.__class__}: {e}')
return False
if p.returncode == 0:
cli.log.info('Successfully cloned %s to %s!', url, destination)
return True
else:
cli.log.error('git clone exited %d', p.returncode)
if p.returncode != 0:
cli.log.error(f'git clone exited {p.returncode}')
return False
cli.log.info(f'Successfully cloned {url} to {destination}!')
return True
+13 -3
View File
@@ -8,6 +8,16 @@ from pathlib import Path
from milc import cli
def AbsPath(arg):
"""Resolve relative paths to the original working directory.
"""
arg = Path(arg)
if not arg.is_absolute():
return (Path(os.environ['ORIG_CWD']) / arg).absolute()
return arg
def is_qmk_firmware(qmk_firmware):
"""Returns True if the given Path() is a qmk_firmware clone.
"""
@@ -81,15 +91,15 @@ def find_qmk_userspace():
if in_qmk_userspace():
return in_qmk_userspace()
if cli.config.user.overlay_dir:
return Path(cli.config.user.overlay_dir).expanduser().resolve()
if 'QMK_USERSPACE' in os.environ:
path = Path(os.environ['QMK_USERSPACE']).expanduser()
if path.exists():
return path.resolve()
return path
if cli.config.user.overlay_dir:
return Path(cli.config.user.overlay_dir).expanduser().resolve()
return Path.home() / 'qmk_userspace'
+32 -26
View File
@@ -4,6 +4,7 @@
This program can be run from anywhere, with or without a qmk_firmware repository. If a qmk_firmware repository can be located we will use that to augment our available subcommands.
"""
import os
from pathlib import Path
import shlex
import subprocess
import sys
@@ -11,9 +12,32 @@ from platform import platform
from traceback import print_exc
import milc
import platformdirs
from . import __version__
from .helpers import find_qmk_firmware, is_qmk_firmware, find_qmk_userspace
from .helpers import find_qmk_firmware, is_qmk_firmware, find_qmk_userspace, is_qmk_userspace
def _get_default_distrib_path():
if 'windows' in platform().lower():
try:
result = milc.cli.run(['cygpath', '-w', '/opt/qmk'])
if result.returncode == 0:
return result.stdout.strip()
except Exception:
pass
return platformdirs.user_data_dir('qmk')
# Ensure the QMK distribution is on the `$PATH` if present. This must be kept in sync with qmk/qmk_firmware.
QMK_DISTRIB_DIR = Path(os.environ.get('QMK_DISTRIB_DIR', _get_default_distrib_path()))
if QMK_DISTRIB_DIR.exists():
os.environ['PATH'] = str(QMK_DISTRIB_DIR / 'bin') + os.pathsep + os.environ['PATH']
# Prepend any user-defined path prefix
if 'QMK_PATH_PREFIX' in os.environ:
os.environ['PATH'] = os.environ['QMK_PATH_PREFIX'] + os.pathsep + os.environ['PATH']
milc.cli.milc_options(version=__version__)
milc.EMOJI_LOGLEVELS['INFO'] = '{fg_blue}Ψ{style_reset_all}'
@@ -49,31 +73,13 @@ def main():
print('Warning: Your Python version is out of date! Some subcommands may not work!')
print('Please upgrade to Python 3.9 or later.')
if 'windows' in platform().lower():
msystem = os.environ.get('MSYSTEM', '')
# Assume the environment isn't workable by default
env_ok = False
# Check if we're using the mingw64/msys2 environment
if 'mingw64' in sys.executable and 'MINGW64' in msystem:
env_ok = True
# Check if we're using a `uv`-based environment
if '\\uv\\' in sys.executable or '/uv/' in sys.executable:
env_ok = True
# If none of the options above were true, then we're in an unsupported environment. Bomb out.
if not env_ok:
print('ERROR: It seems you are not using the MINGW64 terminal.')
print('Please close this terminal and open a new MSYS2 MinGW 64-bit terminal.')
print('Python: %s, MSYSTEM: %s' % (sys.executable, msystem))
exit(1)
# Environment setup
qmk_userspace = find_qmk_userspace()
qmk_firmware = find_qmk_firmware()
os.environ['QMK_USERSPACE'] = str(qmk_userspace)
if is_qmk_userspace(qmk_userspace):
os.environ['QMK_USERSPACE'] = str(qmk_userspace)
elif 'QMK_USERSPACE' in os.environ: # Failed to find valid userspace, including what was in the environment if specified -- wipe any environment variable if present as it's clearly invalid.
os.environ.pop('QMK_USERSPACE')
os.environ['QMK_HOME'] = str(qmk_firmware)
os.environ['ORIG_CWD'] = os.getcwd()
@@ -90,9 +96,9 @@ def main():
except ImportError as e:
if qmk_firmware.name != 'qmk_firmware':
print('Warning: %s does not end in "qmk_firmware". Do you need to set QMK_HOME to "%s/qmk_firmware"?' % (qmk_firmware, qmk_firmware))
print(f'Warning: {qmk_firmware} does not end in "qmk_firmware". Do you need to set QMK_HOME to "{qmk_firmware}/qmk_firmware"?')
print('Error: %s: %s', (e.__class__.__name__, e))
print(f'Error: {e.__class__}: {e}')
print_exc()
sys.exit(1)
@@ -104,7 +110,7 @@ def main():
elif return_code is not True and isinstance(return_code, int):
if return_code < 0 or return_code > 255:
milc.cli.log.error('Invalid return_code: %d', return_code)
milc.cli.log.error(f'Invalid return_code: {return_code}')
exit(255)
exit(return_code)
+6 -9
View File
@@ -5,6 +5,7 @@ from pathlib import Path
from milc import cli
from qmk_cli.git import git_clone
from qmk_cli.helpers import AbsPath
default_repo = 'qmk_firmware'
default_fork = 'qmk/' + default_repo
@@ -12,20 +13,16 @@ default_branch = 'master'
@cli.argument('--baseurl', arg_only=True, default='https://github.com', help='The URL all git operations start from (Default: https://github.com)')
@cli.argument('-b', '--branch', arg_only=True, default=default_branch, help='The branch to clone. Default: %s' % default_branch)
@cli.argument('destination', arg_only=True, default=None, nargs='?', help='The directory to clone to. Default: (current directory)')
@cli.argument('fork', arg_only=True, default=default_fork, nargs='?', help='The qmk_firmware fork to clone. Default: %s' % default_fork)
@cli.argument('-b', '--branch', arg_only=True, default=default_branch, help=f'The branch to clone. Default: {default_branch}')
@cli.argument('destination', arg_only=True, default=Path(os.environ['ORIG_CWD']) / default_repo, type=AbsPath, nargs='?', help='The directory to clone to. Default: (current directory)')
@cli.argument('fork', arg_only=True, default=default_fork, nargs='?', help=f'The qmk_firmware fork to clone. Default: {default_fork}')
@cli.subcommand('Clone a qmk_firmware fork.')
def clone(cli):
if not cli.args.destination:
cli.args.destination = os.path.join(os.environ['ORIG_CWD'], default_fork.split('/')[-1])
qmk_firmware = Path(cli.args.destination)
git_url = '/'.join((cli.args.baseurl, cli.args.fork))
# Exists (but not an empty dir)
if qmk_firmware.exists() and any(qmk_firmware.iterdir()):
cli.log.error('Destination already exists: %s', cli.args.destination)
if cli.args.destination.exists() and any(cli.args.destination.iterdir()):
cli.log.error(f'Destination already exists: {cli.args.destination}')
exit(1)
return git_clone(git_url, cli.args.destination, cli.args.branch)
+60 -37
View File
@@ -57,6 +57,26 @@ KNOWN_BOOTLOADERS = {
}
def _get_next_color():
"""Cycles to the next available log color.
"""
color = LOG_COLOR['colors'][LOG_COLOR['next']]
LOG_COLOR['next'] = (LOG_COLOR['next'] + 1) % len(LOG_COLOR['colors'])
return color
def _gen_header(dev):
"""Generate a consistent logging segment.
"""
return f'{dev["color"]}{dev["manufacturer_string"]} {dev["product_string"]}{{style_reset_all}} ({dev["color"]}{int2hex(dev["vendor_id"])}:{int2hex(dev["product_id"])}:{dev["index"]}{{style_reset_all}})'
def int2hex(number):
"""Returns a string representation of the number as hex.
"""
return f'{number:04X}'
def install_deps():
"""Install the necessary dependencies for qmk console.
"""
@@ -69,9 +89,10 @@ def install_deps():
elif 'windows' in this_platform:
command = ['pacboy', 'sync', '--needed', '--noconfirm', '--disable-download-timeout', 'hidapi:x']
else:
cli.log.error('Unsupported platform: %s', this_platform)
cli.log.error(f'Unsupported platform: {this_platform}')
if yesno("Would you like to run `%s` to install the necessary package?", ' '.join(command)):
cmd_str = ' '.join(command)
if yesno(f'Would you like to run "{cmd_str}" to install the necessary package?'):
cli.run(command, capture_output=False)
return True
@@ -85,7 +106,7 @@ def import_usb_core():
return usb.core
except ImportError as e:
cli.log.error('Could not import usb.core: %s', e)
cli.log.error(f'Could not import usb.core: {e}')
if install_deps():
return import_usb_core()
@@ -96,17 +117,31 @@ def import_usb_core():
def import_hid():
"""Attempts to import the hid module.
"""
import os
old_cwd = os.getcwd()
try:
# macOS library search paths don't include homebrew by default when launched via a `uv`-based deployment.
# The `hid` python module does a search and attempts to load a set of known library names, but doesn't qualify the path, nor gives the option to do so.
# To work around this, we chdir to the homebrew lib directory before importing hid, then chdir back to where we were.
if 'darwin' in platform().lower() or 'macos' in platform().lower():
for path in ('/opt/homebrew/lib', '/usr/local/lib'):
lib_search = Path(path) / 'libhidapi.dylib'
if lib_search.exists():
os.chdir(path)
break
import hid
return hid
except ImportError as e:
cli.log.error('Could not import hid: %s', e)
cli.log.error(f'Could not import hid: {e}')
if install_deps():
return import_hid()
raise
finally:
os.chdir(old_cwd)
class MonitorDevice(object):
@@ -117,8 +152,7 @@ class MonitorDevice(object):
self.device = self.hid.Device(path=hid_device['path'])
self.current_line = ''
cli.log.info('Console Connected: %(color)s%(manufacturer_string)s %(product_string)s{style_reset_all} (%(color)s%(vendor_id)04X:%(product_id)04X:%(index)d{style_reset_all})', hid_device)
cli.log.info(f'Console Connected: {_gen_header(hid_device)}')
def read(self, size, encoding='ascii', timeout=1):
"""Read size bytes from the device.
"""
@@ -138,12 +172,10 @@ class MonitorDevice(object):
def run_forever(self):
while True:
try:
message = {**self.hid_device, 'text': self.read_line()}
identifier = (int2hex(message['vendor_id']), int2hex(message['product_id'])) if self.numeric else (message['manufacturer_string'], message['product_string'])
message['identifier'] = ':'.join(identifier)
message['ts'] = '{style_dim}{fg_green}%s{style_reset_all} ' % (strftime(cli.config.general.datetime_fmt),) if cli.args.timestamp else ''
text = self.read_line()
ts = f'{{style_dim}}{{fg_green}}{strftime(cli.config.general.datetime_fmt)}{{style_reset_all}}' if cli.args.timestamp else ''
cli.echo('%s', '%(ts)s%(color)s%(identifier)s:%(index)d{style_reset_all}: %(text)s' % message)
cli.echo(f'{ts}{_gen_header(self.hid_device)}: {text}')
except self.hid.HIDException:
break
@@ -167,13 +199,12 @@ class FindDevices(object):
try:
for device in list(live_devices):
if not live_devices[device]['thread'].is_alive():
cli.log.info('Console Disconnected: %(color)s%(manufacturer_string)s %(product_string)s{style_reset_all} (%(color)s%(vendor_id)04X:%(product_id)04X:%(index)d{style_reset_all})', live_devices[device])
cli.log.info(f'Console Disconnected: {_gen_header(live_devices[device])}')
del live_devices[device]
for device in self.find_devices():
if device['path'] not in live_devices:
device['color'] = LOG_COLOR['colors'][LOG_COLOR['next']]
LOG_COLOR['next'] = (LOG_COLOR['next'] + 1) % len(LOG_COLOR['colors'])
device['color'] = _get_next_color()
live_devices[device['path']] = device
try:
@@ -182,9 +213,7 @@ class FindDevices(object):
device['thread'].start()
except Exception as e:
device['e'] = e
device['e_name'] = e.__class__.__name__
cli.log.error("Could not connect to %(color)s%(manufacturer_string)s %(product_string)s{style_reset_all} (%(color)s%(vendor_id)04X:%(product_id)04X:%(index)d{style_reset_all}): %(e_name)s: %(e)s", device)
cli.log.error(f'Could not connect to {_gen_header(device)}: {e.__class__.__name__}: {e}')
if cli.config.general.verbose:
cli.log.exception(e)
del live_devices[device['path']]
@@ -195,7 +224,7 @@ class FindDevices(object):
live_bootloaders[device.address]._qmk_found = True
else:
name = KNOWN_BOOTLOADERS[(int2hex(device.idVendor), int2hex(device.idProduct))]
cli.log.info('Bootloader Connected: {style_bright}{fg_magenta}%s', name)
cli.log.info(f'Bootloader Connected: {{style_bright}}{{fg_magenta}}{name}')
device._qmk_found = True
live_bootloaders[device.address] = device
@@ -204,7 +233,7 @@ class FindDevices(object):
live_bootloaders[device]._qmk_found = False
else:
name = KNOWN_BOOTLOADERS[(int2hex(live_bootloaders[device].idVendor), int2hex(live_bootloaders[device].idProduct))]
cli.log.info('Bootloader Disconnected: {style_bright}{fg_magenta}%s', name)
cli.log.info(f'Bootloader Disconnected: {{style_bright}}{{fg_magenta}}{name}')
del live_bootloaders[device]
sleep(.1)
@@ -271,23 +300,17 @@ class FindDevices(object):
# Add index numbers
device_index = {}
for device in devices:
id = ':'.join((int2hex(device['vendor_id']), int2hex(device['product_id'])))
ident = ':'.join((int2hex(device['vendor_id']), int2hex(device['product_id'])))
if id not in device_index:
device_index[id] = 0
if ident not in device_index:
device_index[ident] = 0
device_index[id] += 1
device['index'] = device_index[id]
device_index[ident] += 1
device['index'] = device_index[ident]
return devices
def int2hex(number):
"""Returns a string representation of the number as hex.
"""
return "%04X" % number
def list_devices(device_finder):
"""Show the user a nicely formatted list of devices.
"""
@@ -296,9 +319,8 @@ def list_devices(device_finder):
if devices:
cli.log.info('Available devices:')
for dev in devices:
color = LOG_COLOR['colors'][LOG_COLOR['next']]
LOG_COLOR['next'] = (LOG_COLOR['next'] + 1) % len(LOG_COLOR['colors'])
cli.log.info("\t%s%s:%s:%d{style_reset_all}\t%s %s", color, int2hex(dev['vendor_id']), int2hex(dev['product_id']), dev['index'], dev['manufacturer_string'], dev['product_string'])
color = _get_next_color()
cli.log.info(f'\t{color}{int2hex(dev["vendor_id"])}:{int2hex(dev["product_id"])}:{dev["index"]}{{style_reset_all}}\t{dev["manufacturer_string"]} {dev["product_string"]}')
if cli.args.bootloaders:
bootloaders = device_finder.find_bootloaders()
@@ -307,7 +329,8 @@ def list_devices(device_finder):
cli.log.info('Available Bootloaders:')
for dev in bootloaders:
cli.log.info("\t%s:%s\t%s", int2hex(dev.idVendor), int2hex(dev.idProduct), KNOWN_BOOTLOADERS[(int2hex(dev.idVendor), int2hex(dev.idProduct))])
color = _get_next_color()
cli.log.info(f'\t{color}{int2hex(dev.idVendor)}:{int2hex(dev.idProduct)}{{style_reset_all}}\t{KNOWN_BOOTLOADERS[(int2hex(dev.idVendor), int2hex(dev.idProduct))]}')
@cli.argument('--bootloaders', arg_only=True, default=True, action='store_boolean', help='displaying bootloaders.')
@@ -334,17 +357,17 @@ def console(cli):
vid, pid, index = device
if not index.isdigit():
cli.log.error('Device index must be a number! Got "%s" instead.', index)
cli.log.error(f'Device index must be a number! Got "{index}" instead.')
exit(1)
index = int(index)
if index < 1:
cli.log.error('Device index must be greater than 0! Got %s', index)
cli.log.error(f'Device index must be greater than 0! Got "{index}".')
exit(1)
else:
cli.log.error('Invalid format for device, expected "<pid>:<vid>[:<index>]" but got "%s".', cli.config.console.device)
cli.log.error(f'Invalid format for device, expected "<pid>:<vid>[:<index>]" but got "{cli.config.console.device}".')
cli.print_help()
exit(1)
+6 -4
View File
@@ -10,12 +10,14 @@ from qmk_cli.helpers import is_qmk_firmware, is_qmk_userspace
@cli.argument('var', arg_only=True, default=None, nargs='?', help='Optional variable to query')
@cli.subcommand('Prints environment information.')
def env(cli):
home = os.environ.get('QMK_HOME', "")
userspace = os.environ.get('QMK_USERSPACE', "")
home = os.environ.get('QMK_HOME', '')
userspace = os.environ.get('QMK_USERSPACE', '')
data = {
'QMK_HOME': home,
'QMK_FIRMWARE': home if is_qmk_firmware(Path(home)) else "",
'QMK_USERSPACE': userspace if is_qmk_userspace(Path(userspace)) else ""
'QMK_FIRMWARE': home if is_qmk_firmware(Path(home)) else '',
'QMK_USERSPACE': userspace if is_qmk_userspace(Path(userspace)) else '',
'QMK_DISTRIB_DIR': os.environ.get('QMK_DISTRIB_DIR', ''),
'QMK_PATH_PREFIX': os.environ.get('QMK_PATH_PREFIX', '')
}
# Now munge the current cli config
+24 -25
View File
@@ -1,7 +1,6 @@
"""Setup qmk_firmware on your computer.
"""
import os
import shlex
import subprocess
import sys
from pathlib import Path
@@ -10,7 +9,7 @@ from shutil import rmtree
from milc import cli
from milc.questions import choice, question, yesno
from qmk_cli.git import git_clone
from qmk_cli.helpers import is_qmk_firmware
from qmk_cli.helpers import AbsPath, is_qmk_firmware
default_base = 'https://github.com'
default_repo = 'qmk_firmware'
@@ -36,13 +35,13 @@ def git_upstream(destination):
for line in p.stdout:
print(line, end='')
if p.returncode == 0:
cli.log.info('Added %s as remote upstream.', git_url)
return True
else:
cli.log.error('%s exited %d', ' '.join(git_cmd), p.returncode)
if p.returncode != 0:
cli.log.error(f'git remote exited {p.returncode}')
return False
cli.log.info(f'Added {git_url} as remote upstream.')
return True
def git_clone_fork(fork, branch, force=False):
if force:
@@ -57,16 +56,16 @@ def git_clone_fork(fork, branch, force=False):
@cli.argument('-n', '--no', arg_only=True, action='store_true', help='Answer no to all questions')
@cli.argument('-y', '--yes', arg_only=True, action='store_true', help='Answer yes to all questions')
@cli.argument('--baseurl', arg_only=True, default=default_base, help='The URL all git operations start from. Default: %s' % default_base)
@cli.argument('-b', '--branch', arg_only=True, default=default_branch, help='The branch to clone. Default: %s' % default_branch)
@cli.argument('-H', '--home', arg_only=True, default=Path(os.environ['QMK_HOME']), type=Path, help='The location for QMK Firmware. Default: %s' % os.environ['QMK_HOME'])
@cli.argument('fork', arg_only=True, default=default_fork, nargs='?', help='The qmk_firmware fork to clone. Default: %s' % default_fork)
@cli.argument('--baseurl', arg_only=True, default=default_base, help=f'The URL all git operations start from. Default: {default_base}')
@cli.argument('-b', '--branch', arg_only=True, default=default_branch, help=f'The branch to clone. Default: {default_branch}')
@cli.argument('-H', '--home', arg_only=True, default=Path(os.environ['QMK_HOME']), type=AbsPath, help=f'The location for QMK Firmware. Default: {os.environ["QMK_HOME"]}')
@cli.argument('fork', arg_only=True, default=default_fork, nargs='?', help=f'The qmk_firmware fork to clone. Default: {default_fork}')
@cli.subcommand('Setup your computer for qmk_firmware.')
def setup(cli):
"""Guide the user through setting up their QMK environment.
"""
clone_prompt = 'Would you like to clone {fg_cyan}%s{fg_reset} to {fg_cyan}%s{fg_reset}?' % (cli.args.fork, shlex.quote(str(cli.args.home)))
home_prompt = 'Would you like to set {fg_cyan}%s{fg_reset} as your QMK home?' % (shlex.quote(str(cli.args.home)),)
clone_prompt = f'Would you like to clone {{fg_cyan}}{cli.args.fork}{{fg_reset}} to {{fg_cyan}}{cli.args.home}{{fg_reset}}?'
home_prompt = f'Would you like to set {{fg_cyan}}{cli.args.home}{{fg_reset}} as your QMK home?'
# Sanity checks
if cli.args.yes and cli.args.no:
@@ -77,25 +76,25 @@ def setup(cli):
# If it exists, ask the user what to do with it
# If it doesn't exist, offer to check it out
if is_qmk_firmware(cli.args.home):
cli.log.info('Found qmk_firmware at %s.', str(cli.args.home))
found_prompt = "What do you want to do?"
cli.log.info(f'Found qmk_firmware at "{cli.args.home}".')
found_prompt = 'What do you want to do?'
found_options = [
f"Delete and reclone {cli.args.fork}",
"Delete and clone a different fork",
"Keep it and continue"
f'Delete and reclone {cli.args.fork}',
'Delete and clone a different fork',
'Keep it and continue'
]
delete_confirm = "WARNING: This will delete your current qmk_firmware directory. Proceed?"
delete_confirm = 'WARNING: This will delete your current qmk_firmware directory. Proceed?'
found_action = choice(found_prompt, options=found_options, default=2)
if found_action == f"Delete and reclone {cli.args.fork}":
if found_action == f'Delete and reclone {cli.args.fork}':
if not yesno(delete_confirm, default=False):
exit(1)
git_clone_fork(cli.args.fork, cli.args.branch, force=True)
elif found_action == "Delete and clone a different fork":
fork_name = question("Enter the name of the fork:", default=cli.args.fork)
branch_name = question("Enter the branch name to clone:", default=cli.args.branch)
elif found_action == 'Delete and clone a different fork':
fork_name = question('Enter the name of the fork:', default=cli.args.fork)
branch_name = question('Enter the branch name to clone:', default=cli.args.branch)
if not yesno(delete_confirm, default=False):
exit(1)
@@ -107,9 +106,9 @@ def setup(cli):
path_str = str(cli.args.home)
if cli.args.home.name != 'qmk_firmware':
cli.log.warning('Warning: %s does not end in "qmk_firmware". Did you mean to use "--home %s/qmk_firmware"?' % (path_str, path_str))
cli.log.warning(f'Warning: {path_str} does not end in "qmk_firmware". Did you mean to use "--home {path_str}/qmk_firmware"?')
cli.log.error("Path '%s' exists but is not a qmk_firmware clone!", path_str)
cli.log.error(f'Path "{path_str}" exists but is not a qmk_firmware clone!')
exit(1)
else:
-7
View File
@@ -1,7 +0,0 @@
#!/bin/bash
# Increment the verison number and release a new version of qmk_cli.
#
# Required packages: pip3 install bumpversion twine
echo "Use the github action instead!"
exit 1
-1
View File
@@ -1,4 +1,3 @@
build
bumpversion
requests
twine
+2 -1
View File
@@ -9,7 +9,7 @@ ignore = E501,E226
[metadata]
name = qmk
version = 1.1.7
version = 1.2.0
author = skullydazed
author_email = skullydazed@gmail.com
description = A program to help users work with QMK Firmware.
@@ -38,6 +38,7 @@ classifiers =
install_requires =
hid
milc>=1.9.0
platformdirs
pyusb
# qmk_firmware packages
dotty-dict
-29
View File
@@ -1,29 +0,0 @@
#!/usr/bin/env python3
"""Trigger workflows that build QMK packages.
"""
from os import environ
from pathlib import Path
import requests
# Pull in environment vars
gh_user = environ.get('GH_USERNAME', 'qmk-bot')
gh_pat = environ.get('QMK_BOT_TOKEN', '')
gh_repo_owner = environ.get('REPO_OWNER', 'qmk')
gh_repo_name = environ.get('REPO_NAME', 'qmk_fpm')
gh_ref = environ.get('BRANCH_NAME', 'main')
gh_workflow_ids = environ.get('WORKFLOW_IDS', 'all-trigger.yml').split(',')
gh_api_url = environ.get('GITHUB_API_URL', 'https://api.github.com')
gh_workflow_args = {'ref': gh_ref}
gh_workflow_headers = {'Accept': 'application/vnd.github.v3+json'}
for gh_workflow_id in gh_workflow_ids:
gh_workflow_endpoint = f'{gh_api_url}/repos/{gh_repo_owner}/{gh_repo_name}/actions/workflows/{gh_workflow_id}/dispatches'
print(f'Triggering {gh_workflow_id} workflow at: {gh_workflow_endpoint}')
workflow_dispatch = requests.post(gh_workflow_endpoint, headers=gh_workflow_headers, json=gh_workflow_args, auth=(gh_user, gh_pat))
if workflow_dispatch.status_code != 204:
print(f'Error from GitHub API, status_code {workflow_dispatch.status_code}!')
print(workflow_dispatch.text)
exit(1)
exit(0)