Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
7267306
Bump pip in devcontainer
spoorcc Oct 11, 2025
ff46eda
Pin upload action
spoorcc Oct 11, 2025
6ab4ec1
Add tooling for building with Nuitka
spoorcc Oct 11, 2025
ee3ffc2
Create & release standalone binaries
spoorcc Oct 11, 2025
8fbb692
Unpin deps in build.yaml
spoorcc Oct 11, 2025
9bdd871
Use 3.13 for build
spoorcc Oct 11, 2025
2afe2f6
Hide debug output
spoorcc Oct 11, 2025
d3243d2
Split test to clean environment
spoorcc Oct 11, 2025
70325be
Fix build
spoorcc Oct 11, 2025
5c1c7b6
Fix build
spoorcc Oct 11, 2025
3443839
Patchelf is already installed & ccache unneeded for ephermeral agent
spoorcc Oct 11, 2025
048ae65
Optimize binary
spoorcc Oct 11, 2025
1c41986
Fix path in binary test
spoorcc Oct 11, 2025
528ccdf
Fix build with symlink
spoorcc Oct 11, 2025
e6318dc
Make sure dfetch can be executed
spoorcc Oct 11, 2025
0af09be
Don't return non-zero exit code if tool not found during environment
spoorcc Oct 11, 2025
7609f0d
Cleanup build script
spoorcc Oct 11, 2025
db6a529
Add ccache that caches between builds
spoorcc Oct 11, 2025
8f7024d
Also find projects with comments
spoorcc Oct 11, 2025
32d7c40
There is no example folder
spoorcc Oct 11, 2025
77cac4d
Try configuring ccache
spoorcc Oct 11, 2025
a6645b2
Pin more actions
spoorcc Oct 11, 2025
a49bde7
Don't fail the build if the exact same test version already exists
spoorcc Oct 11, 2025
22ac492
Add verbose ccache
spoorcc Oct 11, 2025
cfaf389
Improve ccache settings
spoorcc Oct 11, 2025
31aa02a
Build binaries on windows & macos
spoorcc Oct 11, 2025
ec42d66
Make artifacts unique
spoorcc Oct 12, 2025
fff51a0
Add more logging
spoorcc Oct 12, 2025
a5f40be
Don't limit jobs an dhopefully speed up jobs
spoorcc Oct 12, 2025
c1ba4aa
Test binaries on all OS'es
spoorcc Oct 12, 2025
87b7007
ccache action should work on all platforms
spoorcc Oct 12, 2025
0c4c802
Update changelog
spoorcc Oct 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ FROM mcr.microsoft.com/devcontainers/python:3.13-bullseye@sha256:e6d1214434cb015
# Install dependencies
# pv is required for asciicasts
RUN apt-get update && apt-get install --no-install-recommends -y \
ccache=4.2-1 \
pv=1.6.6-1+b1 \
patchelf=0.12-1 \
subversion=1.14.1-3+deb11u2 && \
rm -rf /var/lib/apt/lists/*

Expand All @@ -20,8 +22,8 @@ ENV PYTHONUSERBASE="/home/dev/.local"

COPY --chown=dev:dev . .

RUN pip install --no-cache-dir --root-user-action=ignore --upgrade pip==25.1.1 \
&& pip install --no-cache-dir --root-user-action=ignore -e .[development,docs,test,casts] \
RUN pip install --no-cache-dir --root-user-action=ignore --upgrade pip==25.2 \
&& pip install --no-cache-dir --root-user-action=ignore -e .[development,docs,test,casts,build] \
&& pre-commit install --install-hooks

# Set bash as the default shell
Expand Down
108 changes: 108 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
name: Build

on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened]

permissions:
contents: read

jobs:

build:
strategy:
matrix:
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
permissions:
contents: read
security-events: write

steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit

- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0

- name: Setup Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: '3.13'

- name: ccache
uses: hendrikmuhs/ccache-action@30ae3502c7f2d3200209bf2f90eccef2357896cf # v1.2
with:
key: ${{ github.job }}-${{ matrix.platform }}
verbose: 1
create-symlink: ${{ matrix.platform != 'windows-latest' }}

- name: Create binary
env:
CCACHE_BASEDIR: ${{ github.workspace }}
CCACHE_NOHASHDIR: true
NUITKA_CACHE_DIR_CCACHE: $HOME/.ccache
NUITKA_CCACHE_BINARY: /usr/bin/ccache
run: |
pip install .[build]
python script/build.py

- name: Store the distribution packages
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: binary-distribution-${{ matrix.platform }}
path: build/dfetch-*

test-binary:
name: test binary
needs:
- build
strategy:
matrix:
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}

steps:
- name: Download the binary artifact
uses: actions/download-artifact@4a24838f3d5601fd639834081e118c2995d51e1c # v5
with:
name: binary-distribution-${{ matrix.platform }}
path: .

- name: Prepare binary
if: matrix.platform == 'ubuntu-latest'
run: |
binary=$(ls dfetch-*-x86_64)
ln -sf "$binary" dfetch
chmod +x dfetch
ls -la .
shell: bash

- name: Prepare binary
if: matrix.platform == 'macos-latest'
run: |
binary=$(ls dfetch-*-osx)
ln -sf "$binary" dfetch
chmod +x dfetch
ls -la .
shell: bash

- name: Prepare binary on Windows
if: matrix.platform == 'windows-latest'
run: |
$binary = Get-ChildItem dfetch-*.exe | Select-Object -First 1
Copy-Item $binary -Destination dfetch.exe -Force
Get-ChildItem
shell: pwsh

- run: ./dfetch init
- run: ./dfetch environment
- run: ./dfetch validate
- run: ./dfetch check
- run: ./dfetch update
- run: ./dfetch update
- run: ./dfetch report -t sbom
11 changes: 6 additions & 5 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
- name: Build a binary wheel and a source tarball
run: python3 -m build
- name: Store the distribution packages
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: python-package-distributions
path: dist/
Expand All @@ -59,14 +59,15 @@ jobs:

steps:
- name: Download all the dists
uses: actions/download-artifact@v5
uses: actions/download-artifact@4a24838f3d5601fd639834081e118c2995d51e1c # v5
with:
name: python-package-distributions
path: dist/
- name: Publish distribution 📦 to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@ab69e431e9c9f48a3310be0a56527c679f56e04d # v1
with:
repository-url: https://test.pypi.org/legacy/
skip-existing: true

- name: Test install from TestPyPI
run: |
Expand All @@ -86,9 +87,9 @@ jobs:

steps:
- name: Download all the dists
uses: actions/download-artifact@v5
uses: actions/download-artifact@4a24838f3d5601fd639834081e118c2995d51e1c # v5
with:
name: python-package-distributions
path: dist/
- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@ab69e431e9c9f48a3310be0a56527c679f56e04d # v1
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Release 0.11.0 (unreleased)
* Add evidence to sbom report (#788)
* Let action work outside of dfetch repo (#816)
* Handle SVN tags with special characters (#811)
* Don't return non-zero exit code if tool not found during environment (#701)
* Create standalone binaries for Linux, Mac & Windows (#705)

Release 0.10.0 (released 2025-03-12)
====================================
Expand Down
4 changes: 3 additions & 1 deletion dfetch/manifest/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,9 @@ def find_name_in_manifest(self, name: str) -> ManifestEntryLocation:
raise FileNotFoundError("No manifest text available")

for line_nr, line in enumerate(self.__text.splitlines(), start=1):
match = re.search(rf"^\s+-\s*name:\s*(?P<name>{re.escape(name)})\s*$", line)
match = re.search(
rf"^\s+-\s*name:\s*(?P<name>{re.escape(name)})\s*#?.*$", line
)

if match:
return ManifestEntryLocation(
Expand Down
11 changes: 8 additions & 3 deletions dfetch/project/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,14 @@ def revision_is_enough() -> bool:
@staticmethod
def list_tool_info() -> None:
"""Print out version information."""
tool, version = get_git_version()

VCS._log_tool(tool, version)
try:
tool, version = get_git_version()
VCS._log_tool(tool, version)
except RuntimeError as exc:
logger.debug(
f"Something went wrong trying to get the version of git: {exc}"
)
VCS._log_tool("git", "<not found in PATH>")

def _fetch_impl(self, version: Version) -> Version:
"""Get the revision of the remote and place it at the local path."""
Expand Down
9 changes: 8 additions & 1 deletion dfetch/project/svn.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,14 @@ def _list_of_tags(self) -> List[str]:
@staticmethod
def list_tool_info() -> None:
"""Print out version information."""
result = run_on_cmdline(logger, "svn --version")
try:
result = run_on_cmdline(logger, "svn --version")
except RuntimeError as exc:
logger.debug(
f"Something went wrong trying to get the version of svn: {exc}"
)
VCS._log_tool("svn", "<not found in PATH>")
return

first_line = result.stdout.decode().split("\n")[0]
tool, version = first_line.replace(",", "").split("version", maxsplit=1)
Expand Down
25 changes: 24 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ docs = [
]
test = ['pytest==8.4.2', 'pytest-cov==7.0.0', 'behave==1.3.3']
casts = ['asciinema==2.4.0']

build = [
'nuitka==2.7.16',
"tomli; python_version < '3.11'", # Tomllib is default in 3.11, required for letting codespell read the pyproject.toml]
]

[project.scripts]
dfetch = "dfetch.__main__:main"
Expand Down Expand Up @@ -167,3 +170,23 @@ standard = ["dfetch", "features"]
reportMissingImports = false
reportMissingModuleSource = false
pythonVersion = "3.9"

[tool.nuitka]
mode = "onefile" # Switch this between standalone and onefile as needed
# jobs = "4" # Can be used to reduce memory usage, in case of compilation issues
# Enable below for debugging
# show-progress = true
assume-yes-for-downloads = true

include-package-data="dfetch,infer_license"
include-module="infer_license.licenses"

# python-flag = ["-OO"] # Cannot optimize (yet) commands rely on __doc__ being present

output-dir = "build"
output-filename-win = "dfetch-{VERSION}.exe"
output-filename-linux = "dfetch-{VERSION}-x86_64"
output-filename-macos = "dfetch-{VERSION}-osx"

# windows-icon-from-ico = "static/favicon.ico"
# windows-company-name = "dfetch-org"
72 changes: 72 additions & 0 deletions script/build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""This script builds the dfetch executable using Nuitka."""
import subprocess # nosec
Comment thread
spoorcc marked this conversation as resolved.
import sys
import tomllib as toml
from typing import Union

from dfetch import __version__


def parse_option(
option_name: str, option_value: Union[bool, str, list, dict]
) -> list[str]:
"""
Convert a config value to Nuitka CLI arguments.

Handles booleans (--flag), strings (--flag=value), lists (multiple --flag=value),
and nested dicts (--flag=key1=val1=key2=val2).

Returns:
list[str]: Nuitka CLI arguments in the format ['--flag', '--key=value']
"""
args = []
cli_key = f"--{option_name.replace('_','-')}"

if isinstance(option_value, bool):
if option_value:
args.append(cli_key)
elif isinstance(option_value, str):
args.append(f"{cli_key}={option_value}".replace("{VERSION}", __version__))
elif isinstance(option_value, list):
for v in option_value:
if isinstance(v, dict):
parts = [f"{v[k]}" for k in v]
args.append(f"{cli_key}={'='.join(parts)}")
else:
args.append(f"{cli_key}={v}")
else:
args.append(f"{cli_key}={option_value}")

return args


# Load pyproject.toml
with open("pyproject.toml", "rb") as pyproject_file:
pyproject = toml.load(pyproject_file)
nuitka_opts = pyproject.get("tool", {}).get("nuitka", {})
Comment thread
spoorcc marked this conversation as resolved.


if sys.platform.startswith("win"):
nuitka_opts["output-filename"] = nuitka_opts["output-filename-win"]
elif sys.platform.startswith("linux"):
nuitka_opts["output-filename"] = nuitka_opts["output-filename-linux"]
elif sys.platform.startswith("darwin"):
nuitka_opts["output-filename"] = nuitka_opts["output-filename-macos"]
Comment thread
spoorcc marked this conversation as resolved.


nuitka_opts = {
k: v
for k, v in nuitka_opts.items()
if k
not in {"output-filename-win", "output-filename-linux", "output-filename-macos"}
}

command = [sys.executable, "-m", "nuitka"]
for key, value in nuitka_opts.items():
command.extend(parse_option(key, value))

command.append("dfetch")

print(command)
subprocess.check_call(command) # nosec
41 changes: 41 additions & 0 deletions tests/test_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from dfetch.manifest.manifest import (
Manifest,
ManifestDict,
ManifestEntryLocation,
RequestedProjectNotFoundError,
find_manifest,
get_childmanifests,
Expand Down Expand Up @@ -177,3 +178,43 @@ def test_single_suggestion_not_found() -> None:
exception = RequestedProjectNotFoundError(["irst", "1234"], ["first", "other"])

assert ["first"] == exception._guess_project(["irst", "1234"])


@pytest.mark.parametrize(
"name, manifest, project_name, result",
[
(
"match",
" - name: foo",
"foo",
ManifestEntryLocation(line_number=1, start=10, end=12),
),
(
"no match",
" - name: foo",
"baz",
RuntimeError,
),
(
"with comment",
" - name: foo # some comment",
"foo",
ManifestEntryLocation(line_number=1, start=10, end=12),
),
(
"no spaces",
" -name:foo #some comment",
"foo",
ManifestEntryLocation(line_number=1, start=8, end=10),
),
],
)
def test_get_manifest_location(name, manifest, project_name, result) -> None:

manifest = Manifest(DICTIONARY_MANIFEST, text=manifest)

if result == RuntimeError:
with pytest.raises(RuntimeError):
manifest.find_name_in_manifest(project_name)
else:
assert manifest.find_name_in_manifest(project_name) == result
Loading