Compare commits

..

38 Commits

Author SHA1 Message Date
zvecr ebfc4e5812 Align git helper funcs 2026-07-02 23:13:45 +01:00
zvecr 33eddecd6e shell cleanup should quote TMPDIR defensively 2026-07-02 06:08:25 +01:00
zvecr ee779cec4d Fix git_clone_fork exit code regression 2026-07-01 04:25:50 +01:00
zvecr df7bd253d4 Add initial test suite 2026-07-01 03:04:46 +01:00
Joel Challis 8c6339493d Handle errors on Windows where some of the files have their read-only bit set (#242) 2026-07-01 02:48:30 +01:00
Joel Challis b87b18e9d4 Delete .flake8 2026-07-01 01:01:34 +01:00
Joel Challis e7b88acea2 Update docker workflow to actually run tests inside built container (#237) 2026-06-29 07:44:33 +01:00
Joel Challis 45c8ad6709 Run test script on Windows (#241) 2026-06-29 06:28:53 +01:00
Joel Challis 2116dd9582 Migrate to ruff for python linting (#240) 2026-06-29 06:10:55 +01:00
Joel Challis 05d1c36f49 Migrate setup.cfg to pyproject.toml (#236) 2026-06-29 04:07:19 +01:00
Joel Challis 5e57fdefe0 Add 'slim' container (#231) 2026-06-29 04:06:02 +01:00
dependabot[bot] af2ef5d90f Bump actions/checkout from 6 to 7 (#239)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [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/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  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-06-20 23:56:18 +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
31 changed files with 1318 additions and 392 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 1.1.7 current_version = 1.2.0
commit = True commit = True
tag = True tag = True
tag_name = {new_version} tag_name = {new_version}
@@ -9,4 +9,4 @@ message = New release: {current_version} → {new_version}
[bumpversion:file:qmk_cli/__init__.py] [bumpversion:file:qmk_cli/__init__.py]
[bumpversion:file:setup.cfg] [bumpversion:file:pyproject.toml]
+41
View File
@@ -0,0 +1,41 @@
name: CLI Lint
permissions:
contents: read
on:
push:
branches:
- master
pull_request:
paths:
- 'qmk_cli/**'
- 'pyproject.toml'
- '.github/workflows/cli_lint.yml'
jobs:
lint:
runs-on: ubuntu-slim
steps:
- uses: actions/setup-python@v6
with:
python-version: '3.9'
- name: Activate virtualenv
shell: bash
run: |
python -m venv .ci_venv
source .ci_venv/bin/activate
echo PATH=$PATH >> $GITHUB_ENV
- uses: actions/checkout@v7
- name: Install dependencies
run: python -m pip install --group dev
- name: Run lint
run: ruff check --no-cache --output-format=github
- name: Run format
run: yapf -r --diff qmk_cli test
+68 -23
View File
@@ -11,58 +11,58 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: ghcr.io/qmk/qmk_base_container container: ghcr.io/qmk/qmk_base_container
env:
QMK_HOME: ~/qmk_firmware
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: Install dependencies - name: Install dependencies
run: apt-get update && apt-get install -y python3-venv run: apt-get update && apt-get install -y python3-venv
- name: Run unit test
shell: bash
run: |
python3 -m venv .ci_venv
source .ci_venv/bin/activate
python3 -m pip install -r requirements.txt
python3 -m pip install --group dev
pytest
- name: Run ci_tests - name: Run ci_tests
run: ./ci_tests -a run: ./ci_tests
test_cli_linux_macos: test_cli_linux_macos:
needs: test_cli_base_container needs: test_cli_base_container
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
env:
QMK_HOME: ~/qmk_firmware
strategy: strategy:
matrix: matrix:
os: [macos-latest, ubuntu-latest] os: [macos-latest, ubuntu-latest]
python-version: ['3.9', '3.10', '3.11', '3.12'] python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
include:
- os: macos-13
python-version: '3.9'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Run ci_tests - name: Run ci_tests
run: ./ci_tests -a run: ./ci_tests
test_cli_win: test_cli_win:
needs: test_cli_base_container needs: test_cli_base_container
runs-on: windows-latest runs-on: windows-latest
env:
QMK_HOME: $HOME/qmk_firmware
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: (MSYS2) Setup and install dependencies - name: (MSYS2) Setup and install dependencies
uses: msys2/setup-msys2@v2 uses: msys2/setup-msys2@v2
with: with:
update: true update: true
install: git mingw-w64-x86_64-toolchain mingw-w64-x86_64-python-pip mingw-w64-x86_64-python-build mingw-w64-x86_64-python-pillow mingw-w64-x86_64-rust install: git mingw-w64-x86_64-toolchain mingw-w64-x86_64-python-pip mingw-w64-x86_64-python-build mingw-w64-x86_64-python-pillow mingw-w64-x86_64-rust mingw-w64-x86_64-python-halo mingw-w64-x86_64-python-nh3 mingw-w64-x86_64-python-rpds-py mingw-w64-x86_64-python-ruff
# Upgrade pip due to msys packaging + pypa/build/pull/736 issues # Upgrade pip due to msys packaging + pypa/build/pull/736 issues
- name: (MSYS2) Install Python dependencies - name: (MSYS2) Install Python dependencies
@@ -70,11 +70,56 @@ jobs:
run: | run: |
python3 -m pip install --break-system-packages --force-reinstall --upgrade pip python3 -m pip install --break-system-packages --force-reinstall --upgrade pip
- name: (MSYS2) Install QMK CLI from source - name: Run ci_tests
shell: msys2 {0} shell: msys2 {0}
run: ./ci_tests
test_docker_container:
needs: test_cli_base_container
runs-on: ubuntu-latest
env:
TEST_TAG: qmkfm/qmk_cli:test
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@v7
- name: Install dependencies
run: |
python3 -m pip install --upgrade pip
pip install setuptools wheel
pip install --group dev
- name: Build Python
run: | run: |
python3 -m build python3 -m build
python3 -m pip install --break-system-packages dist/qmk-*.tar.gz
- name: (MSYS2) Run qmk setup -y - name: Build Test Container
shell: msys2 {0} uses: docker/build-push-action@v7
run: qmk setup -y with:
context: .
load: true
tags: ${{ env.TEST_TAG }}
- name: Test
run: |
docker run --rm -v ${{ github.workspace }}:/qmk_cli -w /qmk_cli ${{ env.TEST_TAG }} /qmk_cli/ci_tests
- name: Build All Containers
uses: docker/build-push-action@v7
with:
context: .
target: full
platforms: linux/amd64,linux/arm64
+4 -4
View File
@@ -39,11 +39,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v7
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v4
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # 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). # 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) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v3 uses: github/codeql-action/autobuild@v4
# ️ Command-line programs to run using the OS shell. # ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@@ -68,4 +68,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v4
+28 -10
View File
@@ -10,47 +10,65 @@ jobs:
steps: steps:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: '3.9' python-version: '3.9'
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- uses: actions/checkout@v4 - uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
- name: 'Get Previous tag' - name: Get Previous tag
id: previoustag id: previoustag
uses: "WyriHaximus/github-action-get-previous-tag@v1" uses: "WyriHaximus/github-action-get-previous-tag@v2"
- name: Download previous artifact - name: Download previous artifact
run: | run: |
pip download -d "${GITHUB_WORKSPACE}/dist" qmk=="${{ steps.previoustag.outputs.tag }}" pip download -d "${GITHUB_WORKSPACE}/dist" qmk=="${{ steps.previoustag.outputs.tag }}"
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Build and Push to Docker Hub - name: Build and Push slim to Docker Hub
uses: docker/build-push-action@v6.15.0 uses: docker/build-push-action@v7
with: with:
context: . context: .
push: true push: true
target: slim
platforms: linux/amd64,linux/arm64
tags: |
ghcr.io/qmk/qmk_cli:slim
qmkfm/qmk_cli:slim
ghcr.io/qmk/qmk_cli:${{ steps.previoustag.outputs.tag }}-slim
qmkfm/qmk_cli:${{ steps.previoustag.outputs.tag }}-slim
- name: Build and Push full to Docker Hub
uses: docker/build-push-action@v7
with:
context: .
push: true
target: full
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
tags: | tags: |
ghcr.io/qmk/qmk_cli:latest ghcr.io/qmk/qmk_cli:latest
qmkfm/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 }}
+28 -16
View File
@@ -18,31 +18,29 @@ jobs:
steps: steps:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: '3.9' python-version: '3.9'
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: Run ci_tests
run: ./ci_tests
- name: Install dependencies - name: Install dependencies
run: | run: |
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
pip install setuptools wheel pip install setuptools wheel
pip install -r requirements-dev.txt pip install --group dev
- name: Bump version - name: Bump version
run: | run: |
git config --local user.email "hello@qmk.fm" git config --local user.email "hello@qmk.fm"
git config --local user.name "QMK Bot" 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 }} bumpversion ${{ github.event.inputs.version_part }}
- name: Push changes - name: Push changes
@@ -61,29 +59,43 @@ jobs:
twine upload dist/* twine upload dist/*
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Build and Push to Docker Hub - name: Build and Push slim to Docker Hub
uses: docker/build-push-action@v6.15.0 uses: docker/build-push-action@v7
with: with:
context: . context: .
push: true push: true
target: slim
platforms: linux/amd64,linux/arm64
tags: |
ghcr.io/qmk/qmk_cli:slim
qmkfm/qmk_cli:slim
ghcr.io/qmk/qmk_cli:${{ env.NEW_CLI_VERSION }}-slim
qmkfm/qmk_cli:${{ env.NEW_CLI_VERSION }}-slim
- name: Build and Push full to Docker Hub
uses: docker/build-push-action@v7
with:
context: .
push: true
target: full
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
tags: | tags: |
ghcr.io/qmk/qmk_cli:latest ghcr.io/qmk/qmk_cli:latest
qmkfm/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 }}
+38 -5
View File
@@ -1,13 +1,46 @@
FROM ghcr.io/qmk/qmk_base_container:latest FROM ghcr.io/qmk/qmk_base_container:latest AS builder
# Copy package in # Copy package in
ADD dist /tmp/dist 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 # Install python packages
RUN python3 -m pip uninstall -y qmk || true RUN python3 -m pip uninstall -y qmk || true
RUN python3 -m pip install --upgrade pip setuptools wheel nose2 && \ RUN python3 -m pip install --upgrade pip setuptools wheel nose2 && \
python3 -m pip install /tmp/dist/qmk-*.whl && \ python3 -m pip install /tmp/dist/qmk-*.whl
rm -rf /tmp/dist
# 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 AS slim
COPY --from=builder /opt/uv /opt/uv
# Do the equivalent of entering the virtual environment
ENV PATH=/opt/uv/tools/qmk/bin:$PATH \
VIRTUAL_ENV=/opt/uv/tools/qmk \
QMK_HOME=/qmk_firmware \
QMK_USERSPACE=/qmk_userspace
# 2nd stage so we don't have /tmp/dist in the final image
FROM ghcr.io/qmk/qmk_base_container:latest AS full
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
+16 -18
View File
@@ -1,9 +1,12 @@
# QMK CLI # 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 * Interact with your qmk_firmware tree from any location
* Use `qmk clone` to pull down anyone's `qmk_firmware` fork * Use `qmk clone` to pull down anyone's `qmk_firmware` fork
@@ -17,36 +20,31 @@ A program to help users work with [QMK Firmware](https://qmk.fm/).
* `qmk lint` * `qmk lint`
* ...and many more! * ...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. We provide "install and go" packages for many Operating Systems.
## Linux ### macOS
Packages for several distributions available here: https://github.com/qmk/qmk_fpm
## macOS
Using [Homebrew](https://brew.sh): Using [Homebrew](https://brew.sh):
brew install qmk/qmk/qmk brew install qmk/qmk/qmk
## Windows ### Windows
Download our custom MSYS2 installer here: https://msys.qmk.fm/ Download our custom MSYS2 installer here: https://msys.qmk.fm/
# Quickstart ## Building
* `python3 -m pip install qmk`
* `qmk setup`
# Building
We follow PEP517, you can install using [build](https://pypi.org/project/build/): We follow PEP517, you can install using [build](https://pypi.org/project/build/):
Setup: Setup:
python3 -m pip install build python3 -m pip install --group dev
Build: 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). 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> Full documentation: <https://docs.qmk.fm/#/tutorial>
+10 -44
View File
@@ -3,57 +3,23 @@
set -e set -e
#set -x #set -x
ADVANCED=false
TMPDIR=$(mktemp -d) TMPDIR=$(mktemp -d)
QMK_HOME="$TMPDIR/qmk_firmware"
export QMK_HOME
for arg in $@; do trap 'rm -rf "$TMPDIR"' EXIT
if [ "$arg" = "-a" ]; then
ADVANCED=true
fi
done
if [ $ADVANCED = true ]; then CI_VENV="$TMPDIR/.ci_venv"
echo "*** Running in advanced mode with tmp files in $TMPDIR"
else # Avoid issues with building pillow as we install mingw-w64-x86_64-python-pillow
echo "*** Running in basic mode with tmp files in $TMPDIR" if [[ "$(uname -s)" =~ ^MINGW64_NT.* ]]; then
CI_VENV_ARGS="--system-site-packages"
fi fi
# Setup our virtualenv python3 -m venv $CI_VENV_ARGS $CI_VENV
if [ -e .ci_venv ]; then source $CI_VENV/bin/activate
rm -rf .ci_venv
fi
python3 -m venv .ci_venv
source .ci_venv/bin/activate
# Install dependencies # Install dependencies
python3 -m pip install -U pip wheel python3 -m pip install -U pip wheel
python3 -m pip install . python3 -m pip install .
python3 -m pip install -r requirements-dev.txt python3 -m pip install --group dev
# Ensure that qmk works pytest -vv -m system --qmk=real
echo "*** Testing 'qmk clone -h'"
qmk clone -h
#echo "*** Testing 'qmk config -a'" # Test disabled as `milc` at least 1.6.8+ returns False and thus non-zero exit code
#qmk config -a
echo "*** Testing 'qmk setup -n'"
qmk setup -n
echo
echo "*** Basic tests completed successfully!"
# Run advanced test if requested
if [ $ADVANCED = true ]; then
echo
echo "*** Testing 'qmk setup -y'"
qmk setup -y
echo
echo "*** Advanced tests completed successfully!"
fi
# Cleanup
deactivate
rm -rf .ci_venv $TMPDIR
+144 -4
View File
@@ -1,6 +1,146 @@
[build-system] [build-system]
requires = [ requires = ["setuptools>=82.0.1"]
"setuptools>=42",
"wheel"
]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project]
name = "qmk"
version = "1.2.0"
license = "MIT"
readme = "README.md"
authors = [{name = "skullydazed", email = "skullydazed@gmail.com"}]
description = "A program to help users work with QMK Firmware."
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"Intended Audience :: End Users/Desktop",
"Natural Language :: English",
"Programming Language :: Python :: 3 :: Only",
"Topic :: Scientific/Engineering",
"Topic :: Software Development",
"Topic :: Utilities",
]
requires-python = ">=3.9"
dependencies = [
"hid",
"milc>=1.9.0",
"platformdirs",
"pyusb",
# qmk_firmware packages
"dotty-dict",
"hjson",
"jsonschema>=4",
"pillow",
"pygments",
"pyserial",
]
[dependency-groups]
dev = [
"build",
"bumpversion",
"pytest",
"ruff",
"twine",
"yapf",
]
[project.urls]
"Bug Tracker" = "https://github.com/qmk/qmk_cli/issues"
Documentation = "https://docs.qmk.fm/cli"
Homepage = "https://qmk.fm/"
Source = "https://github.com/qmk/qmk_cli/"
[project.scripts]
qmk = "qmk_cli.script_qmk:main"
[tool.setuptools]
include-package-data = false
[tool.setuptools.packages]
find = {namespaces = false}
[tool.ruff]
target-version = "py39"
include = [
"qmk_cli/**/*.py",
"test/**/*.py",
]
[tool.ruff.lint]
select = ["E", "F", "B", "N", "W", "COM", "C901", "PGH004"]
ignore = [
"E501", # QMK is ok with long lines.
"E231", # Conflicts with our yapf config
]
[tool.ruff.lint.mccabe]
max-complexity = 14 # Let's slowly crank this down
[tool.yapfignore]
ignore_patterns = [
".direnv/**/*",
".env/**/*",
]
[tool.yapf]
align_closing_bracket_with_visual_indent = true
allow_multiline_dictionary_keys = false
allow_multiline_lambdas = false
allow_split_before_default_or_named_assigns = true
allow_split_before_dict_value = true
arithmetic_precedence_indication = true
blank_lines_around_top_level_definition = 2
blank_line_before_class_docstring = false
blank_line_before_module_docstring = false
blank_line_before_nested_class_or_def = false
coalesce_brackets = true
column_limit = 256
continuation_align_style = "SPACE"
continuation_indent_width = 4
dedent_closing_brackets = true
disable_ending_comma_heuristic = false
each_dict_entry_on_separate_line = true
i18n_comment = ""
i18n_function_call = ""
indent_blank_lines = false
indent_dictionary_value = true
indent_width = 4
join_multiple_lines = false
no_spaces_around_selected_binary_operators = ""
spaces_around_default_or_named_assign = false
spaces_around_power_operator = false
spaces_before_comment = 2
space_between_ending_comma_and_closing_bracket = false
split_all_comma_separated_values = false
split_arguments_when_comma_terminated = true
split_before_arithmetic_operator = false
split_before_bitwise_operator = true
split_before_closing_bracket = true
split_before_dict_set_generator = true
split_before_dot = false
split_before_expression_after_opening_paren = false
split_before_first_argument = false
split_before_logical_operator = false
split_before_named_assigns = true
split_complex_comprehension = true
split_penalty_after_opening_bracket = 300
split_penalty_after_unary_operator = 10000
split_penalty_arithmetic_operator = 300
split_penalty_before_if_expr = 0
split_penalty_bitwise_operator = 300
split_penalty_comprehension = 80
split_penalty_excess_character = 7000
split_penalty_for_added_line_split = 30
split_penalty_import_names = 0
split_penalty_logical_operator = 300
use_tabs = false
[tool.pytest.ini_options]
markers = [
"integration",
"system",
]
addopts = "-m 'not (integration or system)'"
+1
View File
@@ -2,4 +2,5 @@
"""Wrapper to call the qmk cli script entrypoint. """Wrapper to call the qmk cli script entrypoint.
""" """
import qmk_cli.script_qmk import qmk_cli.script_qmk
qmk_cli.script_qmk.main() qmk_cli.script_qmk.main()
+1 -1
View File
@@ -1,3 +1,3 @@
"""A program to help you work with qmk_firmware.""" """A program to help you work with qmk_firmware."""
__version__ = '1.1.7' __version__ = '1.2.0'
+59 -15
View File
@@ -1,24 +1,24 @@
"""Helpers for working with git. """Helpers for working with git."""
"""
import subprocess import subprocess
from .helpers import rmtree
from milc import cli from milc import cli
default_repo = 'qmk_firmware' DEFAULT_UPSTREAM = 'https://github.com/qmk/qmk_firmware'
default_fork = 'qmk/' + default_repo
default_branch = 'master'
def git_clone(url, destination, branch): def git_clone(destination, url, branch):
"""Add the qmk/qmk_firmware upstream to a qmk_firmware clone."""
git_clone = [ git_clone = [
'git', 'git',
'clone', 'clone',
'--recurse-submodules', '--recurse-submodules',
'--branch=' + branch, f'--branch={branch}',
url, url,
str(destination), str(destination),
] ]
cli.log.debug('Git clone command: %s', git_clone) cli.log.debug(f'Git clone command: {git_clone}')
try: try:
with subprocess.Popen(git_clone, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True, encoding='utf-8') as p: with subprocess.Popen(git_clone, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True, encoding='utf-8') as p:
@@ -28,13 +28,57 @@ def git_clone(url, destination, branch):
except Exception as e: except Exception as e:
git_cmd = ' '.join([s.replace(' ', r'\ ') for s in git_clone]) 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__.__name__}:{e}")
return False return False
if p.returncode == 0: if p.returncode != 0:
cli.log.info('Successfully cloned %s to %s!', url, destination) cli.log.error(f'git clone exited {p.returncode}')
return True
else:
cli.log.error('git clone exited %d', p.returncode)
return False return False
cli.log.info(f'Successfully cloned {url} to {destination}!')
return True
def git_set_upstream(destination):
"""Add the qmk/qmk_firmware upstream to a qmk_firmware clone."""
git_remote = [
'git',
'-C',
str(destination),
'remote',
'add',
'upstream',
DEFAULT_UPSTREAM,
]
cli.log.debug(f'Git remote command: {git_remote}')
try:
with subprocess.Popen(git_remote, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True, encoding='utf-8') as p:
for line in p.stdout:
print(line, end='')
except Exception as e:
git_cmd = ' '.join([s.replace(' ', r'\ ') for s in git_remote])
cli.log.error(f"Could not run '{git_cmd}': {e.__class__.__name__}:{e}")
return False
if p.returncode != 0:
cli.log.error(f'git remote add exited {p.returncode}')
return False
cli.log.info(f'Added {DEFAULT_UPSTREAM} as remote upstream.')
return True
def git_clone_fork(destination, baseurl, fork, branch, force=False):
"""Clone fork and configure upstream."""
url = '/'.join((baseurl, fork))
if force:
rmtree(destination)
if not git_clone(destination, url, branch):
return False
return git_set_upstream(destination)
+32 -5
View File
@@ -1,13 +1,40 @@
"""Useful helper functions. """Useful helper functions.
""" """
import os import os
import sys
import stat
import json import json
import shutil
from functools import lru_cache from functools import lru_cache
from pathlib import Path from pathlib import Path
from milc import cli from milc import cli
def AbsPath(arg): # noqa: N802
"""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 rmtree(path):
"""Safe version of shutil.rmtree which handles errors on Windows where some of the files have their read-only bit set."""
def remove_readonly(func, path, _):
"""Clear the readonly bit and reattempt the removal."""
os.chmod(path, stat.S_IWRITE)
func(path)
if sys.version_info >= (3, 12):
# See https://docs.python.org/3.12/whatsnew/3.12.html#shutil
return shutil.rmtree(path, onexc=remove_readonly)
else:
return shutil.rmtree(path, onerror=remove_readonly)
def is_qmk_firmware(qmk_firmware): def is_qmk_firmware(qmk_firmware):
"""Returns True if the given Path() is a qmk_firmware clone. """Returns True if the given Path() is a qmk_firmware clone.
""" """
@@ -16,7 +43,7 @@ def is_qmk_firmware(qmk_firmware):
qmk_firmware / 'quantum', qmk_firmware / 'quantum',
qmk_firmware / 'requirements.txt', qmk_firmware / 'requirements.txt',
qmk_firmware / 'requirements-dev.txt', qmk_firmware / 'requirements-dev.txt',
qmk_firmware / 'lib/python/qmk/cli/__init__.py' qmk_firmware / 'lib/python/qmk/cli/__init__.py',
] ]
for path in paths: for path in paths:
@@ -70,7 +97,7 @@ def is_qmk_userspace(qmk_userspace):
try: try:
return 'userspace_version' in json.loads(path.read_text(encoding="UTF-8")) return 'userspace_version' in json.loads(path.read_text(encoding="UTF-8"))
except json.decoder.JSONDecodeError as e: except json.decoder.JSONDecodeError:
return False return False
@@ -81,15 +108,15 @@ def find_qmk_userspace():
if in_qmk_userspace(): if in_qmk_userspace():
return 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: if 'QMK_USERSPACE' in os.environ:
path = Path(os.environ['QMK_USERSPACE']).expanduser() path = Path(os.environ['QMK_USERSPACE']).expanduser()
if path.exists(): if path.exists():
return path.resolve() return path.resolve()
return path return path
if cli.config.user.overlay_dir:
return Path(cli.config.user.overlay_dir).expanduser().resolve()
return Path.home() / 'qmk_userspace' 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. 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 import os
from pathlib import Path
import shlex import shlex
import subprocess import subprocess
import sys import sys
@@ -11,15 +12,38 @@ from platform import platform
from traceback import print_exc from traceback import print_exc
import milc import milc
import platformdirs
from . import __version__ 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.cli.milc_options(version=__version__)
milc.EMOJI_LOGLEVELS['INFO'] = '{fg_blue}Ψ{style_reset_all}' milc.EMOJI_LOGLEVELS['INFO'] = '{fg_blue}Ψ{style_reset_all}'
# These must happen after the milc.milc_options() call # These must happen after the milc.milc_options() call
import milc.subcommand.config # noqa, must come after milc.milc_options() import milc.subcommand.config # noqa: E402, must come after milc.milc_options()
@milc.cli.entrypoint('CLI wrapper for running QMK commands.') @milc.cli.entrypoint('CLI wrapper for running QMK commands.')
@@ -49,35 +73,17 @@ def main():
print('Warning: Your Python version is out of date! Some subcommands may not work!') print('Warning: Your Python version is out of date! Some subcommands may not work!')
print('Please upgrade to Python 3.9 or later.') 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 # Environment setup
qmk_userspace = find_qmk_userspace() qmk_userspace = find_qmk_userspace()
qmk_firmware = find_qmk_firmware() 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['QMK_HOME'] = str(qmk_firmware)
os.environ['ORIG_CWD'] = os.getcwd() os.environ['ORIG_CWD'] = os.getcwd()
import qmk_cli.subcommands import qmk_cli.subcommands # noqa: F401
# Check out and initialize the qmk_firmware environment # Check out and initialize the qmk_firmware environment
if is_qmk_firmware(qmk_firmware): if is_qmk_firmware(qmk_firmware):
@@ -86,7 +92,7 @@ def main():
sys.path.append(str(qmk_firmware / 'lib/python')) sys.path.append(str(qmk_firmware / 'lib/python'))
try: try:
import qmk.cli # noqa import qmk.cli # noqa: F401
except ImportError as e: except ImportError as e:
if qmk_firmware.name != 'qmk_firmware': if qmk_firmware.name != 'qmk_firmware':
+4 -4
View File
@@ -2,7 +2,7 @@
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.
""" """
from . import clone # noqa from . import clone # noqa: F401
from . import console # noqa from . import console # noqa: F401
from . import env # noqa from . import env # noqa: F401
from . import setup # noqa from . import setup # noqa: F401
+5 -8
View File
@@ -5,6 +5,7 @@ from pathlib import Path
from milc import cli from milc import cli
from qmk_cli.git import git_clone from qmk_cli.git import git_clone
from qmk_cli.helpers import AbsPath
default_repo = 'qmk_firmware' default_repo = 'qmk_firmware'
default_fork = 'qmk/' + default_repo default_fork = 'qmk/' + default_repo
@@ -13,19 +14,15 @@ 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('--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('-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('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='The qmk_firmware fork to clone. Default: %s' % default_fork) @cli.argument('fork', arg_only=True, default=default_fork, nargs='?', help='The qmk_firmware fork to clone. Default: %s' % default_fork)
@cli.subcommand('Clone a qmk_firmware fork.') @cli.subcommand('Clone a qmk_firmware fork.')
def clone(cli): 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)) git_url = '/'.join((cli.args.baseurl, cli.args.fork))
# Exists (but not an empty dir) # Exists (but not an empty dir)
if qmk_firmware.exists() and any(qmk_firmware.iterdir()): if cli.args.destination.exists() and any(cli.args.destination.iterdir()):
cli.log.error('Destination already exists: %s', cli.args.destination) cli.log.error('Destination already exists: %s', cli.args.destination)
exit(1) return False
return git_clone(git_url, cli.args.destination, cli.args.branch) return git_clone(cli.args.destination, git_url, cli.args.branch)
+15 -1
View File
@@ -53,7 +53,7 @@ KNOWN_BOOTLOADERS = {
('2A03', '0036'): 'caterina: Arduino Leonardo', ('2A03', '0036'): 'caterina: Arduino Leonardo',
('2A03', '0037'): 'caterina: Arduino Micro', ('2A03', '0037'): 'caterina: Arduino Micro',
('314B', '0106'): 'apm32-dfu: APM32 DFU ISP Mode', ('314B', '0106'): 'apm32-dfu: APM32 DFU ISP Mode',
('342D', 'DFA0'): 'wb32-dfu: WB32 Device in DFU Mode' ('342D', 'DFA0'): 'wb32-dfu: WB32 Device in DFU Mode',
} }
@@ -96,7 +96,19 @@ def import_usb_core():
def import_hid(): def import_hid():
"""Attempts to import the hid module. """Attempts to import the hid module.
""" """
import os
old_cwd = os.getcwd()
try: 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 import hid
return hid return hid
@@ -107,6 +119,8 @@ def import_hid():
return import_hid() return import_hid()
raise raise
finally:
os.chdir(old_cwd)
class MonitorDevice(object): class MonitorDevice(object):
+8 -1
View File
@@ -1,6 +1,7 @@
"""Prints environment information. """Prints environment information.
""" """
import os import os
import sys
from pathlib import Path from pathlib import Path
from milc import cli from milc import cli
@@ -15,7 +16,9 @@ def env(cli):
data = { data = {
'QMK_HOME': home, 'QMK_HOME': home,
'QMK_FIRMWARE': home if is_qmk_firmware(Path(home)) else "", 'QMK_FIRMWARE': home if is_qmk_firmware(Path(home)) else "",
'QMK_USERSPACE': userspace if is_qmk_userspace(Path(userspace)) 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 # Now munge the current cli config
@@ -24,6 +27,10 @@ def env(cli):
data[converted_key] = val data[converted_key] = val
if cli.args.var: if cli.args.var:
if cli.args.var not in data:
print(f'Variable "{cli.args.var}" does not exist!', file=sys.stderr)
return False
# dump out requested arg # dump out requested arg
print(data[cli.args.var]) print(data[cli.args.var])
else: else:
+22 -58
View File
@@ -2,67 +2,28 @@
""" """
import os import os
import shlex import shlex
import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
from shutil import rmtree
from milc import cli from milc import cli
from milc.questions import choice, question, yesno from milc.questions import choice, question, yesno
from qmk_cli.git import git_clone from qmk_cli.git import git_clone_fork
from qmk_cli.helpers import is_qmk_firmware from qmk_cli.helpers import AbsPath, is_qmk_firmware
default_base = 'https://github.com' DEFAULT_BASE = 'https://github.com'
default_repo = 'qmk_firmware' DEFAULT_REPO = 'qmk_firmware'
default_fork = 'qmk/' + default_repo DEFAULT_FORK = 'qmk/' + DEFAULT_REPO
default_branch = 'master' DEFAULT_BRANCH = 'master'
def git_upstream(destination):
"""Add the qmk/qmk_firmware upstream to a qmk_firmware clone.
"""
git_url = '/'.join((cli.args.baseurl, default_fork))
git_cmd = [
'git',
'-C',
destination,
'remote',
'add',
'upstream',
git_url,
]
with subprocess.Popen(git_cmd, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True, encoding='utf-8') as p:
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)
return False
def git_clone_fork(fork, branch, force=False):
if force:
rmtree(cli.args.home)
git_url = '/'.join((cli.args.baseurl, fork))
if git_clone(git_url, cli.args.home, branch):
git_upstream(cli.args.home)
else:
exit(1)
@cli.argument('-n', '--no', arg_only=True, action='store_true', help='Answer no to all questions') @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('-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('--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('-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('-H', '--home', arg_only=True, default=Path(os.environ['QMK_HOME']), type=AbsPath, 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('fork', arg_only=True, default=DEFAULT_FORK, nargs='?', help='The qmk_firmware fork to clone. Default: %s' % DEFAULT_FORK)
@cli.subcommand('Setup your computer for qmk_firmware.') @cli.subcommand('Setup your computer for qmk_firmware.')
def setup(cli): def setup(cli): # noqa: C901
"""Guide the user through setting up their QMK environment. """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))) 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)))
@@ -71,7 +32,7 @@ def setup(cli):
# Sanity checks # Sanity checks
if cli.args.yes and cli.args.no: if cli.args.yes and cli.args.no:
cli.log.error("Can't use both --yes and --no at the same time.") cli.log.error("Can't use both --yes and --no at the same time.")
exit(1) return False
# Check on qmk_firmware # Check on qmk_firmware
# If it exists, ask the user what to do with it # If it exists, ask the user what to do with it
@@ -82,25 +43,27 @@ def setup(cli):
found_options = [ found_options = [
f"Delete and reclone {cli.args.fork}", f"Delete and reclone {cli.args.fork}",
"Delete and clone a different fork", "Delete and clone a different fork",
"Keep it and continue" "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) 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): if not yesno(delete_confirm, default=False):
exit(1) return False
git_clone_fork(cli.args.fork, cli.args.branch, force=True) if not git_clone_fork(cli.args.home, cli.args.baseurl, cli.args.fork, cli.args.branch, force=True):
return False
elif found_action == "Delete and clone a different fork": elif found_action == "Delete and clone a different fork":
fork_name = question("Enter the name of the fork:", default=cli.args.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) branch_name = question("Enter the branch name to clone:", default=cli.args.branch)
if not yesno(delete_confirm, default=False): if not yesno(delete_confirm, default=False):
exit(1) return False
git_clone_fork(fork_name, branch_name, force=True) if not git_clone_fork(cli.args.home, cli.args.baseurl, fork_name, branch_name, force=True):
return False
# Exists (but not an empty dir) # Exists (but not an empty dir)
elif cli.args.home.exists() and any(cli.args.home.iterdir()): elif cli.args.home.exists() and any(cli.args.home.iterdir()):
@@ -110,12 +73,13 @@ def setup(cli):
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('Warning: %s does not end in "qmk_firmware". Did you mean to use "--home %s/qmk_firmware"?' % (path_str, path_str))
cli.log.error("Path '%s' exists but is not a qmk_firmware clone!", path_str) cli.log.error("Path '%s' exists but is not a qmk_firmware clone!", path_str)
exit(1) return False
else: else:
cli.log.error('Could not find qmk_firmware!') cli.log.error('Could not find qmk_firmware!')
if yesno(clone_prompt): if yesno(clone_prompt):
git_clone_fork(cli.args.fork, cli.args.branch) if not git_clone_fork(cli.args.home, cli.args.baseurl, cli.args.fork, cli.args.branch):
return False
else: else:
cli.log.warning('Not cloning qmk_firmware due to user input or --no flag.') cli.log.warning('Not cloning qmk_firmware due to user input or --no flag.')
-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
-4
View File
@@ -1,4 +0,0 @@
build
bumpversion
requests
twine
-107
View File
@@ -1,107 +0,0 @@
#[bumpversion]
# Bumpversion config has been moved to .bumpversion.cfg
[bdist_wheel]
universal = 1
[flake8]
ignore = E501,E226
[metadata]
name = qmk
version = 1.1.7
author = skullydazed
author_email = skullydazed@gmail.com
description = A program to help users work with QMK Firmware.
long_description = file: README.md
long_description_content_type = text/markdown
license = MIT License
project_urls =
Bug Tracker = https://github.com/qmk/qmk_cli/issues
Documentation = https://docs.qmk.fm/#/cli
Homepage = https://qmk.fm/
Source = https://github.com/qmk/qmk_cli/
classifiers =
Development Status :: 3 - Alpha
Environment :: Console
Intended Audience :: Developers
Intended Audience :: System Administrators
Intended Audience :: End Users/Desktop
License :: OSI Approved :: MIT License
Natural Language :: English
Programming Language :: Python :: 3 :: Only
Topic :: Scientific/Engineering
Topic :: Software Development
Topic :: Utilities
[options]
install_requires =
hid
milc>=1.9.0
pyusb
# qmk_firmware packages
dotty-dict
hjson
jsonschema>=4
pillow
pygments
pyserial
packages = find:
python_requires = >=3.9
[options.entry_points]
console_scripts =
qmk = qmk_cli.script_qmk:main
[yapf]
align_closing_bracket_with_visual_indent = True
allow_multiline_dictionary_keys = False
allow_multiline_lambdas = False
allow_split_before_default_or_named_assigns = True
allow_split_before_dict_value = True
arithmetic_precedence_indication = True
blank_lines_around_top_level_definition = 2
blank_line_before_class_docstring = False
blank_line_before_module_docstring = False
blank_line_before_nested_class_or_def = False
coalesce_brackets = True
column_limit = 256
continuation_align_style = SPACE
continuation_indent_width = 4
dedent_closing_brackets = True
disable_ending_comma_heuristic = False
each_dict_entry_on_separate_line = True
i18n_comment =
i18n_function_call =
indent_blank_lines = False
indent_dictionary_value = True
indent_width = 4
join_multiple_lines = False
no_spaces_around_selected_binary_operators =
spaces_around_default_or_named_assign = False
spaces_around_power_operator = False
spaces_before_comment = 2
space_between_ending_comma_and_closing_bracket = False
split_all_comma_separated_values = False
split_arguments_when_comma_terminated = True
split_before_arithmetic_operator = False
split_before_bitwise_operator = True
split_before_closing_bracket = True
split_before_dict_set_generator = True
split_before_dot = False
split_before_expression_after_opening_paren = False
split_before_first_argument = False
split_before_logical_operator = False
split_before_named_assigns = True
split_complex_comprehension = True
split_penalty_after_opening_bracket = 300
split_penalty_after_unary_operator = 10000
split_penalty_arithmetic_operator = 300
split_penalty_before_if_expr = 0
split_penalty_bitwise_operator = 300
split_penalty_comprehension = 80
split_penalty_excess_character = 7000
split_penalty_for_added_line_split = 30
split_penalty_import_names = 0
split_penalty_logical_operator = 300
use_tabs = False
+39
View File
@@ -0,0 +1,39 @@
import tempfile
import pytest
from pathlib import Path
from unittest.mock import create_autospec, PropertyMock, MagicMock
from milc import MILCInterface
from milc.configuration import Configuration
def pytest_addoption(parser):
parser.addoption(
'--qmk',
action='store',
default='wrapper',
help='options: ./qmk wrapper or real qmk from path',
choices=('wrapper', 'real'),
)
@pytest.fixture
def temp_directory():
"""Similar behavior to the default tmpdir fixture except the folder is cleaned up."""
with tempfile.TemporaryDirectory() as tmp_path:
yield Path(tmp_path)
@pytest.fixture
def mock_cli():
"""Mock cli to pass to cli subcommands,"""
ret = create_autospec(MILCInterface)
# default behavior
type(ret).config = Configuration()
ret.config.general.unicode = True
ret.config.general.color = True
type(ret).write_config_option = MagicMock()
type(ret.args).var = PropertyMock(return_value=None)
return ret
+40
View File
@@ -0,0 +1,40 @@
import pytest
from unittest.mock import patch, PropertyMock
@pytest.fixture
def subcommand(temp_directory):
tmp = temp_directory.as_posix()
with patch.dict('os.environ', {'ORIG_CWD': tmp, 'QMK_HOME': tmp}, clear=True):
import qmk_cli.subcommands.clone as clone
yield clone
@pytest.fixture
def git_clone(subcommand):
with patch.object(subcommand, 'git_clone') as git_clone:
yield git_clone
def test_clone(subcommand, mock_cli, git_clone):
type(mock_cli.args).baseurl = PropertyMock(return_value='asdf')
type(mock_cli.args).fork = PropertyMock(return_value='fork')
git_clone.return_value = True
ret = subcommand.clone(mock_cli)
git_clone.assert_called_once()
assert ret is True
def test_clone_not_empty(subcommand, mock_cli, temp_directory, git_clone):
type(mock_cli.args).baseurl = PropertyMock(return_value='asdf')
type(mock_cli.args).fork = PropertyMock(return_value='fork')
type(mock_cli.args).destination = PropertyMock(return_value=temp_directory)
(temp_directory / 'asdf').touch()
ret = subcommand.clone(mock_cli)
git_clone.assert_not_called()
assert ret is False
+51
View File
@@ -0,0 +1,51 @@
import pytest
from unittest.mock import patch, PropertyMock
@pytest.fixture
def subcommand(temp_directory):
tmp = temp_directory.as_posix()
with patch.dict('os.environ', {'ORIG_CWD': tmp, 'QMK_HOME': tmp}, clear=True):
import qmk_cli.subcommands.env as env
yield env
@pytest.fixture
def is_qmk_firmware(subcommand):
with patch.object(subcommand, 'is_qmk_firmware') as is_qmk_firmware:
is_qmk_firmware.return_value = True
yield is_qmk_firmware
def test_request_qmk_firmware_exists(subcommand, mock_cli, capsys, temp_directory, is_qmk_firmware):
subcommand.env(mock_cli)
captured = capsys.readouterr().out
assert f'QMK_HOME="{temp_directory}"' in captured
assert f'QMK_FIRMWARE="{temp_directory}"' in captured
def test_request_all_variables(subcommand, mock_cli, capsys, temp_directory):
subcommand.env(mock_cli)
captured = capsys.readouterr().out
assert f'QMK_HOME="{temp_directory}"' in captured
assert 'QMK_FIRMWARE=""' in captured
assert 'QMK_UNICODE="True"' in captured
def test_request_single_variable(subcommand, mock_cli, capsys, temp_directory):
type(mock_cli.args).var = PropertyMock(return_value='QMK_HOME')
subcommand.env(mock_cli)
captured = capsys.readouterr().out
assert captured == f'{temp_directory}\n'
def test_request_single_variable_no_exits(subcommand, mock_cli, capsys):
type(mock_cli.args).var = PropertyMock(return_value='ASDF')
ret = subcommand.env(mock_cli)
captured = capsys.readouterr().err
assert captured == 'Variable "ASDF" does not exist!\n'
assert ret is False
+221
View File
@@ -0,0 +1,221 @@
import pytest
from unittest.mock import patch, PropertyMock, ANY
@pytest.fixture
def subcommand(temp_directory):
tmp = temp_directory.as_posix()
with patch.dict('os.environ', {'ORIG_CWD': tmp, 'QMK_HOME': tmp}, clear=True):
import qmk_cli.subcommands.setup as setup
yield setup
@pytest.fixture
def is_qmk_firmware(subcommand):
with patch.object(subcommand, 'is_qmk_firmware') as is_qmk_firmware:
is_qmk_firmware.return_value = True
yield is_qmk_firmware
@pytest.fixture
def yesno(subcommand):
with patch.object(subcommand, 'yesno') as yesno:
yesno.return_value = True
yield yesno
@pytest.fixture
def choice(subcommand):
with patch.object(subcommand, 'choice') as choice:
yield choice
@pytest.fixture
def question(subcommand):
with patch.object(subcommand, 'question') as question:
yield question
@pytest.fixture
def git_clone_fork(subcommand):
with patch.object(subcommand, 'git_clone_fork') as git_clone_fork:
yield git_clone_fork
def test_setup_both_yes_no_invalid(subcommand, mock_cli):
type(mock_cli.args).yes = PropertyMock(return_value=True)
type(mock_cli.args).no = PropertyMock(return_value=True)
ret = subcommand.setup(mock_cli)
assert ret is False
def test_setup_reclone(subcommand, mock_cli, is_qmk_firmware, temp_directory, yesno, choice, git_clone_fork):
type(mock_cli.args).yes = PropertyMock(return_value=False)
type(mock_cli.args).no = PropertyMock(return_value=False)
type(mock_cli.args).home = PropertyMock(return_value=temp_directory)
type(mock_cli.args).fork = 'asdf'
choice.return_value = 'Delete and reclone asdf'
yesno.return_value = True
subcommand.setup(mock_cli)
yesno.assert_called_once()
assert 'This will delete your current qmk_firmware directory.' in yesno.call_args.args[0]
git_clone_fork.assert_called_once()
def test_setup_reclone_failed(subcommand, mock_cli, is_qmk_firmware, temp_directory, yesno, choice, git_clone_fork):
type(mock_cli.args).yes = PropertyMock(return_value=False)
type(mock_cli.args).no = PropertyMock(return_value=False)
type(mock_cli.args).home = PropertyMock(return_value=temp_directory)
type(mock_cli.args).fork = 'asdf'
choice.return_value = 'Delete and reclone asdf'
yesno.return_value = True
git_clone_fork.return_value = False
ret = subcommand.setup(mock_cli)
yesno.assert_called_once()
assert 'This will delete your current qmk_firmware directory.' in yesno.call_args.args[0]
git_clone_fork.assert_called_once()
assert ret is False
def test_setup_reclone_no(subcommand, mock_cli, is_qmk_firmware, temp_directory, yesno, choice, git_clone_fork):
type(mock_cli.args).yes = PropertyMock(return_value=False)
type(mock_cli.args).no = PropertyMock(return_value=False)
type(mock_cli.args).home = PropertyMock(return_value=temp_directory)
type(mock_cli.args).fork = 'asdf'
choice.return_value = 'Delete and reclone asdf'
yesno.return_value = False
ret = subcommand.setup(mock_cli)
assert ret is False
git_clone_fork.assert_not_called()
def test_setup_clone_diff_fork(subcommand, mock_cli, is_qmk_firmware, temp_directory, yesno, choice, question, git_clone_fork):
type(mock_cli.args).yes = PropertyMock(return_value=False)
type(mock_cli.args).no = PropertyMock(return_value=False)
type(mock_cli.args).home = PropertyMock(return_value=temp_directory)
choice.return_value = 'Delete and clone a different fork'
yesno.return_value = True
subcommand.setup(mock_cli)
yesno.assert_called_once()
assert 'This will delete your current qmk_firmware directory.' in yesno.call_args.args[0]
git_clone_fork.assert_called_once()
def test_setup_clone_diff_fork_failed(subcommand, mock_cli, is_qmk_firmware, temp_directory, yesno, choice, question, git_clone_fork):
type(mock_cli.args).yes = PropertyMock(return_value=False)
type(mock_cli.args).no = PropertyMock(return_value=False)
type(mock_cli.args).home = PropertyMock(return_value=temp_directory)
choice.return_value = 'Delete and clone a different fork'
yesno.return_value = True
git_clone_fork.return_value = False
ret = subcommand.setup(mock_cli)
yesno.assert_called_once()
assert 'This will delete your current qmk_firmware directory.' in yesno.call_args.args[0]
git_clone_fork.assert_called_once()
assert ret is False
def test_setup_clone_diff_fork_no(subcommand, mock_cli, is_qmk_firmware, temp_directory, yesno, choice, question, git_clone_fork):
type(mock_cli.args).yes = PropertyMock(return_value=False)
type(mock_cli.args).no = PropertyMock(return_value=False)
type(mock_cli.args).home = PropertyMock(return_value=temp_directory)
choice.return_value = 'Delete and clone a different fork'
yesno.return_value = False
ret = subcommand.setup(mock_cli)
assert ret is False
git_clone_fork.assert_not_called()
def test_setup_home_exists_not_empty(subcommand, mock_cli, is_qmk_firmware, temp_directory):
type(mock_cli.args).yes = PropertyMock(return_value=False)
type(mock_cli.args).no = PropertyMock(return_value=False)
type(mock_cli.args).home = PropertyMock(return_value=temp_directory)
is_qmk_firmware.return_value = False
(temp_directory / 'asdf').touch()
ret = subcommand.setup(mock_cli)
assert ret is False
def test_setup_missing(subcommand, mock_cli, is_qmk_firmware, temp_directory, yesno, git_clone_fork):
type(mock_cli.args).yes = PropertyMock(return_value=False)
type(mock_cli.args).no = PropertyMock(return_value=False)
type(mock_cli.args).home = PropertyMock(return_value=temp_directory)
is_qmk_firmware.return_value = False
yesno.return_value = True
subcommand.setup(mock_cli)
yesno.assert_called_once()
assert 'Would you like to clone' in yesno.call_args.args[0]
git_clone_fork.assert_called_once()
def test_setup_missing_failed(subcommand, mock_cli, is_qmk_firmware, temp_directory, yesno, git_clone_fork):
type(mock_cli.args).yes = PropertyMock(return_value=False)
type(mock_cli.args).no = PropertyMock(return_value=False)
type(mock_cli.args).home = PropertyMock(return_value=temp_directory)
is_qmk_firmware.return_value = False
yesno.return_value = True
git_clone_fork.return_value = False
ret = subcommand.setup(mock_cli)
yesno.assert_called_once()
assert 'Would you like to clone' in yesno.call_args.args[0]
git_clone_fork.assert_called_once()
assert ret is False
def test_setup_missing_no(subcommand, mock_cli, is_qmk_firmware, temp_directory, yesno, git_clone_fork):
type(mock_cli.args).yes = PropertyMock(return_value=False)
type(mock_cli.args).no = PropertyMock(return_value=False)
type(mock_cli.args).home = PropertyMock(return_value=temp_directory)
is_qmk_firmware.return_value = False
yesno.return_value = False
subcommand.setup(mock_cli)
yesno.assert_called_once()
assert 'Would you like to clone' in yesno.call_args.args[0]
git_clone_fork.assert_not_called()
def test_setup_chains_args_to_doctor(subcommand, mock_cli, is_qmk_firmware, yesno, choice, git_clone_fork):
type(mock_cli.args).yes = PropertyMock(return_value=True)
type(mock_cli.args).no = PropertyMock(return_value=False)
subcommand.setup(mock_cli)
mock_cli.run.assert_called_once()
assert mock_cli.run.call_args.args[0] == [ANY, '--color', '--unicode', 'doctor', '-y']
def test_setup_chains_args_to_doctor_no(subcommand, mock_cli, is_qmk_firmware, yesno, choice, git_clone_fork):
type(mock_cli.args).yes = PropertyMock(return_value=False)
type(mock_cli.args).no = PropertyMock(return_value=True)
mock_cli.config.general.color = False
mock_cli.config.general.unicode = False
subcommand.setup(mock_cli)
mock_cli.run.assert_called_once()
assert mock_cli.run.call_args.args[0] == [ANY, '--no-color', '--no-unicode', 'doctor', '-n']
+46
View File
@@ -0,0 +1,46 @@
import pytest
import subprocess
@pytest.fixture(autouse=True)
def temp_env(temp_directory, monkeypatch):
qmk_firmware = temp_directory / 'qmk_firmware'
monkeypatch.setenv("QMK_HOME", qmk_firmware.as_posix())
yield qmk_firmware
@pytest.fixture
def run_cli(request):
def run_cli_wrapper(args):
cmd = f'{request.config.rootpath}/qmk' if request.config.getoption("--qmk") == 'wrapper' else 'qmk'
return subprocess.run([cmd, *args], capture_output=True, text=True)
return run_cli_wrapper
@pytest.mark.system
def test_cli_help(run_cli):
completed_process = run_cli(['clone', '--help'])
assert 'usage:' in completed_process.stdout
assert completed_process.returncode == 0
@pytest.mark.system
def test_cli_setup_no(run_cli):
completed_process = run_cli(['setup', '-n'])
assert 'Could not find qmk_firmware!' in completed_process.stderr
assert 'Not cloning qmk_firmware due to user input or --no flag.' in completed_process.stderr
assert completed_process.returncode == 0
@pytest.mark.system
def test_cli_setup_yes(run_cli):
completed_process = run_cli(['setup', '-y'])
assert 'Successfully cloned https://github.com/qmk/qmk_firmware' in completed_process.stderr
assert 'Added https://github.com/qmk/qmk_firmware as remote upstream' in completed_process.stderr
assert 'QMK Doctor is checking your environment' in completed_process.stderr
assert completed_process.returncode == 0
+117
View File
@@ -0,0 +1,117 @@
import pytest
import subprocess
from unittest.mock import patch, PropertyMock
import qmk_cli.git
@patch("qmk_cli.git.subprocess.Popen", side_effect=Exception("foobar"))
@patch("qmk_cli.git.cli.log.error")
def test_git_clone_except(mock_log_error, mock_popen):
ret = qmk_cli.git.git_clone('/tmp', 'https://github.com/qmk/qmk_firmware', 'master')
assert "Could not run" in str(mock_log_error.call_args_list)
assert "Exception:foobar" in str(mock_log_error.call_args_list)
assert ret is False
@patch("qmk_cli.git.subprocess.Popen")
def test_git_clone_logs_output(mock_popen, capsys):
type(mock_popen().__enter__()).returncode = PropertyMock(return_value=0)
type(mock_popen().__enter__()).stdout = PropertyMock(return_value=['asdf'])
ret = qmk_cli.git.git_clone('/tmp', 'https://github.com/qmk/qmk_firmware', 'master')
captured = capsys.readouterr().out
assert 'asdf' in captured
assert ret is True
@patch("qmk_cli.git.subprocess.Popen")
def test_git_clone_captures_exit_code(mock_popen):
type(mock_popen().__enter__()).returncode = PropertyMock(return_value=1)
ret = qmk_cli.git.git_clone('/tmp', 'https://github.com/qmk/qmk_firmware', 'master')
assert ret is False
@patch("qmk_cli.git.subprocess.Popen", side_effect=Exception("foobar"))
@patch("qmk_cli.git.cli.log.error")
def test_git_set_upstream_except(mock_log_error, mock_popen):
ret = qmk_cli.git.git_set_upstream('/tmp')
assert "Could not run" in str(mock_log_error.call_args_list)
assert "Exception:foobar" in str(mock_log_error.call_args_list)
assert ret is False
@patch("qmk_cli.git.subprocess.Popen")
def test_git_set_upstream_logs_output(mock_popen, capsys):
type(mock_popen().__enter__()).returncode = PropertyMock(return_value=0)
type(mock_popen().__enter__()).stdout = PropertyMock(return_value=['asdf'])
ret = qmk_cli.git.git_set_upstream('/tmp')
captured = capsys.readouterr().out
assert 'asdf' in captured
assert ret is True
@patch("qmk_cli.git.subprocess.Popen")
def test_git_set_upstream_captures_exit_code(mock_popen):
type(mock_popen().__enter__()).returncode = PropertyMock(return_value=1)
ret = qmk_cli.git.git_set_upstream('/tmp')
assert ret is False
@patch.object(qmk_cli.git, 'git_clone')
@patch.object(qmk_cli.git, 'git_set_upstream')
def test_git_clone_fork_fail(git_set_upstream, git_clone, temp_directory):
git_clone.return_value = False
git_set_upstream.return_value = False
ret = qmk_cli.git.git_clone_fork(temp_directory.as_posix(), 'https://github.com', 'qmk/qmk_firmware', 'master', False)
assert ret is False
@patch.object(qmk_cli.git, 'git_clone')
@patch.object(qmk_cli.git, 'git_set_upstream')
def test_git_clone_fork_force(git_set_upstream, git_clone, temp_directory):
(temp_directory / 'asdf').touch()
git_clone.return_value = True
git_set_upstream.return_value = True
ret = qmk_cli.git.git_clone_fork(temp_directory.as_posix(), 'https://github.com', 'qmk/qmk_firmware', 'master', True)
assert ret is True
assert (temp_directory / 'asdf').exists() is False
@pytest.mark.integration
def test_git_clone(temp_directory):
qmk_firmware = temp_directory / 'qmk_firmware'
ret = qmk_cli.git.git_clone(qmk_firmware.as_posix(), 'https://github.com/qmk/qmk_firmware', 'master')
from qmk_cli.helpers import is_qmk_firmware
assert is_qmk_firmware(qmk_firmware) is True
assert ret is True
@pytest.mark.integration
def test_git_clone_fork(temp_directory):
qmk_firmware = temp_directory / 'qmk_firmware'
qmk_firmware.mkdir()
(qmk_firmware / 'asdf').touch()
ret = qmk_cli.git.git_clone_fork(qmk_firmware.as_posix(), 'https://github.com', 'qmk/qmk_firmware', 'master', True)
from qmk_cli.helpers import is_qmk_firmware
assert is_qmk_firmware(qmk_firmware) is True
assert 'https://github.com/qmk/qmk_firmware' in subprocess.run(['git', '-C', qmk_firmware.as_posix(), 'remote', 'get-url', 'upstream'], capture_output=True, text=True).stdout
assert ret is True
+246
View File
@@ -0,0 +1,246 @@
import os
import stat
import pytest
from pathlib import Path
from unittest.mock import patch, PropertyMock
import qmk_cli.helpers
@pytest.fixture(autouse=True)
def reset_env():
cur_dir = os.getcwd()
yield cur_dir
os.chdir(cur_dir)
qmk_cli.helpers.find_qmk_firmware.cache_clear()
qmk_cli.helpers.in_qmk_firmware.cache_clear()
qmk_cli.helpers.find_qmk_userspace.cache_clear()
qmk_cli.helpers.in_qmk_userspace.cache_clear()
def test_abspath(temp_directory):
tmp = temp_directory.absolute()
ret = qmk_cli.helpers.AbsPath(tmp)
assert ret.samefile(tmp)
def test_abspath_str(temp_directory):
tmp = temp_directory.absolute().as_posix()
ret = qmk_cli.helpers.AbsPath(tmp)
assert ret.samefile(tmp)
def test_abspath_rel(temp_directory):
with patch.dict('os.environ', {'ORIG_CWD': temp_directory.as_posix()}, clear=True):
tmp = './asdf'
ret = qmk_cli.helpers.AbsPath(tmp)
assert ret.as_posix() == (temp_directory / 'asdf').as_posix()
def test_rmtree(temp_directory):
tmp = temp_directory / 'asdf.txt'
tmp.touch()
qmk_cli.helpers.rmtree(temp_directory)
assert tmp.exists() is False
@pytest.mark.skipif(os.name != "nt", reason="Only triggers issue on windows")
def test_rmtree_readonly(temp_directory):
tmp = temp_directory / 'asdf.txt'
tmp.touch()
os.chmod(tmp, stat.S_IREAD)
qmk_cli.helpers.rmtree(temp_directory)
assert tmp.exists() is False
def test_is_qmk_firmware(temp_directory):
(temp_directory / 'quantum').mkdir(parents=True)
(temp_directory / 'lib/python/qmk/cli').mkdir(parents=True)
(temp_directory / 'requirements.txt').touch()
(temp_directory / 'requirements-dev.txt').touch()
(temp_directory / 'lib/python/qmk/cli/__init__.py').touch()
assert qmk_cli.helpers.is_qmk_firmware(temp_directory) is True
def test_is_qmk_firmware_partially_missing(temp_directory):
(temp_directory / 'lib/python/qmk/cli').mkdir(parents=True)
(temp_directory / 'lib/python/qmk/cli/__init__.py').touch()
assert qmk_cli.helpers.is_qmk_firmware(temp_directory) is False
def test_find_qmk_firmware(temp_directory):
(temp_directory / 'quantum').mkdir(parents=True)
(temp_directory / 'lib/python/qmk/cli').mkdir(parents=True)
(temp_directory / 'requirements.txt').touch()
(temp_directory / 'requirements-dev.txt').touch()
(temp_directory / 'lib/python/qmk/cli/__init__.py').touch()
os.chdir(temp_directory)
assert qmk_cli.helpers.find_qmk_firmware().as_posix() == temp_directory.as_posix()
def test_find_qmk_firmware_env(temp_directory):
with patch.dict('os.environ', {'QMK_HOME': temp_directory.as_posix()}, clear=True):
assert qmk_cli.helpers.find_qmk_firmware().as_posix() == temp_directory.as_posix()
def test_find_qmk_firmware_env_missing(temp_directory):
with patch.dict('os.environ', {'QMK_HOME': (temp_directory / 'asdf').as_posix()}, clear=True):
assert qmk_cli.helpers.find_qmk_firmware().as_posix() == (temp_directory / 'asdf').as_posix()
def test_find_qmk_firmware_config(temp_directory):
with patch.object(qmk_cli.helpers, 'cli') as cli:
type(cli.config.user).qmk_home = PropertyMock(return_value=temp_directory.as_posix())
assert qmk_cli.helpers.find_qmk_firmware().as_posix() == temp_directory.as_posix()
def test_find_qmk_firmware_config_user():
with patch.object(qmk_cli.helpers, 'cli') as cli:
type(cli.config.user).qmk_home = PropertyMock(return_value='~/asdf')
assert qmk_cli.helpers.find_qmk_firmware().as_posix() == (Path.home() / 'asdf').as_posix()
def test_find_qmk_firmware_default(temp_directory):
with patch('pathlib.Path.home') as home:
home.return_value = temp_directory
assert qmk_cli.helpers.find_qmk_firmware().as_posix() == (temp_directory / 'qmk_firmware').as_posix()
def test_in_qmk_firmware(temp_directory):
(temp_directory / 'quantum').mkdir(parents=True)
(temp_directory / 'lib/python/qmk/cli').mkdir(parents=True)
(temp_directory / 'requirements.txt').touch()
(temp_directory / 'requirements-dev.txt').touch()
(temp_directory / 'lib/python/qmk/cli/__init__.py').touch()
os.chdir(temp_directory)
assert qmk_cli.helpers.in_qmk_firmware().as_posix() == temp_directory.as_posix()
def test_in_qmk_firmware_sub(temp_directory):
(temp_directory / 'quantum').mkdir(parents=True)
(temp_directory / 'lib/python/qmk/cli').mkdir(parents=True)
(temp_directory / 'requirements.txt').touch()
(temp_directory / 'requirements-dev.txt').touch()
(temp_directory / 'lib/python/qmk/cli/__init__.py').touch()
os.chdir(temp_directory / 'quantum')
assert qmk_cli.helpers.in_qmk_firmware().as_posix() == temp_directory.as_posix()
def test_in_qmk_firmware_sub_sub(temp_directory):
(temp_directory / 'quantum').mkdir(parents=True)
(temp_directory / 'lib/python/qmk/cli').mkdir(parents=True)
(temp_directory / 'requirements.txt').touch()
(temp_directory / 'requirements-dev.txt').touch()
(temp_directory / 'lib/python/qmk/cli/__init__.py').touch()
os.chdir(temp_directory / 'lib' / 'python')
assert qmk_cli.helpers.in_qmk_firmware().as_posix() == temp_directory.as_posix()
def test_in_qmk_firmware_invalid(temp_directory):
os.chdir(temp_directory)
assert qmk_cli.helpers.in_qmk_firmware() is None
def test_is_qmk_userspace(temp_directory):
(temp_directory / 'qmk.json').write_text('{"userspace_version": 2}')
assert qmk_cli.helpers.is_qmk_userspace(temp_directory) is True
def test_is_qmk_userspace_missing(temp_directory):
assert qmk_cli.helpers.is_qmk_userspace(temp_directory) is False
def test_is_qmk_userspace_invalid(temp_directory):
(temp_directory / 'qmk.json').write_text('asdf')
assert qmk_cli.helpers.is_qmk_userspace(temp_directory) is False
def test_is_qmk_userspace_empty(temp_directory):
(temp_directory / 'qmk.json').touch()
assert qmk_cli.helpers.is_qmk_userspace(temp_directory) is False
def test_find_qmk_userspace(temp_directory):
(temp_directory / 'qmk.json').write_text('{"userspace_version": 2}')
os.chdir(temp_directory)
assert qmk_cli.helpers.find_qmk_userspace().as_posix() == temp_directory.as_posix()
def test_find_qmk_userspace_env(temp_directory):
with patch.dict('os.environ', {'QMK_USERSPACE': temp_directory.as_posix()}, clear=True):
assert qmk_cli.helpers.find_qmk_userspace().as_posix() == temp_directory.as_posix()
def test_find_qmk_userspace_env_missing(temp_directory):
with patch.dict('os.environ', {'QMK_USERSPACE': (temp_directory / 'asdf').as_posix()}, clear=True):
assert qmk_cli.helpers.find_qmk_userspace().as_posix() == (temp_directory / 'asdf').as_posix()
def test_find_qmk_userspace_config(temp_directory):
with patch.object(qmk_cli.helpers, 'cli') as cli:
type(cli.config.user).overlay_dir = PropertyMock(return_value=temp_directory.as_posix())
assert qmk_cli.helpers.find_qmk_userspace().as_posix() == temp_directory.as_posix()
def test_find_qmk_userspace_config_user():
with patch.object(qmk_cli.helpers, 'cli') as cli:
type(cli.config.user).overlay_dir = PropertyMock(return_value='~/asdf')
assert qmk_cli.helpers.find_qmk_userspace().as_posix() == (Path.home() / 'asdf').as_posix()
def test_find_qmk_userspace_default(temp_directory):
with patch('pathlib.Path.home') as home:
home.return_value = temp_directory
assert qmk_cli.helpers.find_qmk_userspace().as_posix() == (temp_directory / 'qmk_userspace').as_posix()
def test_in_qmk_userspace(temp_directory):
(temp_directory / 'qmk.json').write_text('{"userspace_version": 2}')
os.chdir(temp_directory)
assert qmk_cli.helpers.in_qmk_userspace().as_posix() == temp_directory.as_posix()
def test_in_qmk_userspace_sub(temp_directory):
(temp_directory / 'qmk.json').write_text('{"userspace_version": 2}')
(temp_directory / 'keymaps' / 'test').mkdir(parents=True)
os.chdir(temp_directory / 'keymaps')
assert qmk_cli.helpers.in_qmk_userspace().as_posix() == temp_directory.as_posix()
def test_in_qmk_userspace_sub_sub(temp_directory):
(temp_directory / 'qmk.json').write_text('{"userspace_version": 2}')
(temp_directory / 'keymaps' / 'test').mkdir(parents=True)
os.chdir(temp_directory / 'keymaps' / 'test')
assert qmk_cli.helpers.in_qmk_userspace().as_posix() == temp_directory.as_posix()
def test_in_qmk_userspace_invalid(temp_directory):
os.chdir(temp_directory)
assert qmk_cli.helpers.in_qmk_userspace() is None
-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)