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]
current_version = 1.1.7
current_version = 1.2.0
commit = True
tag = True
tag_name = {new_version}
@@ -9,4 +9,4 @@ message = New release: {current_version} → {new_version}
[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
container: ghcr.io/qmk/qmk_base_container
env:
QMK_HOME: ~/qmk_firmware
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Install dependencies
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
run: ./ci_tests -a
run: ./ci_tests
test_cli_linux_macos:
needs: test_cli_base_container
runs-on: ${{ matrix.os }}
env:
QMK_HOME: ~/qmk_firmware
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@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Run ci_tests
run: ./ci_tests -a
run: ./ci_tests
test_cli_win:
needs: test_cli_base_container
runs-on: windows-latest
env:
QMK_HOME: $HOME/qmk_firmware
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: (MSYS2) Setup and install dependencies
uses: msys2/setup-msys2@v2
with:
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
- name: (MSYS2) Install Python dependencies
@@ -70,11 +70,56 @@ jobs:
run: |
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}
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: |
python3 -m build
python3 -m pip install --break-system-packages dist/qmk-*.tar.gz
- name: (MSYS2) Run qmk setup -y
shell: msys2 {0}
run: qmk setup -y
- name: Build Test Container
uses: docker/build-push-action@v7
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:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v7
# 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
+28 -10
View File
@@ -10,47 +10,65 @@ 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@v7
with:
fetch-depth: 0
- name: 'Get Previous tag'
- 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
- name: Build and Push slim to Docker Hub
uses: docker/build-push-action@v7
with:
context: .
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
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 }}
+28 -16
View File
@@ -18,31 +18,29 @@ 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
- name: Run ci_tests
run: ./ci_tests
- uses: actions/checkout@v7
- name: Install dependencies
run: |
python3 -m pip install --upgrade pip
pip install setuptools wheel
pip install -r requirements-dev.txt
pip install --group dev
- name: Bump version
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,29 +59,43 @@ 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
- name: Build and Push slim to Docker Hub
uses: docker/build-push-action@v7
with:
context: .
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
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 }}
+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
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 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
[![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,36 +20,31 @@ 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/):
Setup:
python3 -m pip install build
python3 -m pip install --group dev
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>
+10 -44
View File
@@ -3,57 +3,23 @@
set -e
#set -x
ADVANCED=false
TMPDIR=$(mktemp -d)
QMK_HOME="$TMPDIR/qmk_firmware"
export QMK_HOME
for arg in $@; do
if [ "$arg" = "-a" ]; then
ADVANCED=true
fi
done
trap 'rm -rf "$TMPDIR"' EXIT
if [ $ADVANCED = true ]; then
echo "*** Running in advanced mode with tmp files in $TMPDIR"
else
echo "*** Running in basic mode with tmp files in $TMPDIR"
CI_VENV="$TMPDIR/.ci_venv"
# Avoid issues with building pillow as we install mingw-w64-x86_64-python-pillow
if [[ "$(uname -s)" =~ ^MINGW64_NT.* ]]; then
CI_VENV_ARGS="--system-site-packages"
fi
# Setup our virtualenv
if [ -e .ci_venv ]; then
rm -rf .ci_venv
fi
python3 -m venv .ci_venv
source .ci_venv/bin/activate
python3 -m venv $CI_VENV_ARGS $CI_VENV
source $CI_VENV/bin/activate
# Install dependencies
python3 -m pip install -U pip wheel
python3 -m pip install .
python3 -m pip install -r requirements-dev.txt
python3 -m pip install --group dev
# Ensure that qmk works
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
pytest -vv -m system --qmk=real
+144 -4
View File
@@ -1,6 +1,146 @@
[build-system]
requires = [
"setuptools>=42",
"wheel"
]
requires = ["setuptools>=82.0.1"]
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.
"""
import qmk_cli.script_qmk
qmk_cli.script_qmk.main()
+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'
+59 -15
View File
@@ -1,24 +1,24 @@
"""Helpers for working with git.
"""
"""Helpers for working with git."""
import subprocess
from .helpers import rmtree
from milc import cli
default_repo = 'qmk_firmware'
default_fork = 'qmk/' + default_repo
default_branch = 'master'
DEFAULT_UPSTREAM = 'https://github.com/qmk/qmk_firmware'
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',
'--recurse-submodules',
'--branch=' + branch,
f'--branch={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 +28,57 @@ 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__.__name__}:{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
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.
"""
import os
import sys
import stat
import json
import shutil
from functools import lru_cache
from pathlib import Path
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):
"""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 / 'requirements.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:
@@ -70,7 +97,7 @@ def is_qmk_userspace(qmk_userspace):
try:
return 'userspace_version' in json.loads(path.read_text(encoding="UTF-8"))
except json.decoder.JSONDecodeError as e:
except json.decoder.JSONDecodeError:
return False
@@ -81,15 +108,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,15 +12,38 @@ 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}'
# 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.')
@@ -49,35 +73,17 @@ 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()
import qmk_cli.subcommands
import qmk_cli.subcommands # noqa: F401
# Check out and initialize the qmk_firmware environment
if is_qmk_firmware(qmk_firmware):
@@ -86,7 +92,7 @@ def main():
sys.path.append(str(qmk_firmware / 'lib/python'))
try:
import qmk.cli # noqa
import qmk.cli # noqa: F401
except ImportError as e:
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.
"""
from . import clone # noqa
from . import console # noqa
from . import env # noqa
from . import setup # noqa
from . import clone # noqa: F401
from . import console # noqa: F401
from . import env # noqa: F401
from . import setup # noqa: F401
+5 -8
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
@@ -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('-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.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()):
if cli.args.destination.exists() and any(cli.args.destination.iterdir()):
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', '0037'): 'caterina: Arduino Micro',
('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():
"""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
@@ -107,6 +119,8 @@ def import_hid():
return import_hid()
raise
finally:
os.chdir(old_cwd)
class MonitorDevice(object):
+8 -1
View File
@@ -1,6 +1,7 @@
"""Prints environment information.
"""
import os
import sys
from pathlib import Path
from milc import cli
@@ -15,7 +16,9 @@ def env(cli):
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_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,6 +27,10 @@ def env(cli):
data[converted_key] = val
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
print(data[cli.args.var])
else:
+22 -58
View File
@@ -2,67 +2,28 @@
"""
import os
import shlex
import subprocess
import sys
from pathlib import Path
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.git import git_clone_fork
from qmk_cli.helpers import AbsPath, is_qmk_firmware
default_base = 'https://github.com'
default_repo = 'qmk_firmware'
default_fork = 'qmk/' + default_repo
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)
DEFAULT_BASE = 'https://github.com'
DEFAULT_REPO = 'qmk_firmware'
DEFAULT_FORK = 'qmk/' + DEFAULT_REPO
DEFAULT_BRANCH = 'master'
@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='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=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.subcommand('Setup your computer for qmk_firmware.')
def setup(cli):
def setup(cli): # noqa: C901
"""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)))
@@ -71,7 +32,7 @@ def setup(cli):
# Sanity checks
if cli.args.yes and cli.args.no:
cli.log.error("Can't use both --yes and --no at the same time.")
exit(1)
return False
# Check on qmk_firmware
# If it exists, ask the user what to do with it
@@ -82,25 +43,27 @@ def setup(cli):
found_options = [
f"Delete and reclone {cli.args.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?"
found_action = choice(found_prompt, options=found_options, default=2)
if found_action == f"Delete and reclone {cli.args.fork}":
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":
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)
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)
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.error("Path '%s' exists but is not a qmk_firmware clone!", path_str)
exit(1)
return False
else:
cli.log.error('Could not find qmk_firmware!')
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:
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)