Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
84 changes: 84 additions & 0 deletions .claude/commands/dfetch-add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# DFetch add workflow

Add a new project to `dfetch.yaml` and fetch it.
Comment thread
spoorcc marked this conversation as resolved.

The user may pass a URL or description as `$ARGUMENTS`. If they did not, ask for the URL before proceeding.

## Step 1 — Read the manifest

Read `dfetch.yaml` to understand the existing remotes and project patterns.

## Step 2 — Classify the URL

Determine the VCS type from the URL:

- **archive** — URL ends in `.zip`, `.tar.gz`, `.tar.bz2`, `.tar.xz`, or similar.
- **svn** — URL contains `svn`, uses `svn+ssh://`, or the user says so.
- **git** — everything else.

## Step 3 — Gather details

`dfetch add` auto-detects the default branch and guesses a destination, so you only need to ask about things it cannot infer. Use `AskUserQuestion` to collect what you don't yet know:

**For git and SVN:**

Only ask what the user hasn't already told you:
- **Name** — defaults to the repo name from the URL; confirm or let the user override.
- **Destination** (`dst`) — where in the repo the files should land; `dfetch add` guesses from existing project paths, but ask if the user has a preference.
- **Version** — branch, tag, or revision to pin. Leave blank to track the default branch.
- **Source path** (`src`) — sub-path or glob inside the remote repo (e.g. `lib/` or `*.h`). Leave blank to copy everything.
- **Ignore patterns** — glob patterns to exclude. Leave blank for none.

**For archives, also ask:**
- **Source path** (`src`) — sub-directory inside the archive to copy (archives often have a single wrapping top-level directory that dfetch strips automatically).
- **Ignore patterns** — globs to filter out unwanted files (other font families, binary formats, etc.).
- **Integrity hash** — whether to verify the download (strongly recommended; you will compute it).

## Step 4 — Add the project

**Git and SVN** — use the CLI, which appends the entry to `dfetch.yaml` and records the resolved remote:

```bash
dfetch add --name <name> --dst <dst> [--src <src>] [--version <version>] \
[--ignore <p1> <p2> ...] <url>
```

Omit flags for fields the user left blank; `dfetch add` will fill in sensible defaults.

**Archives** — edit `dfetch.yaml` directly with the Edit tool. The CLI does not support `vcs: archive`, `integrity:`, or archive-specific `src` paths. Follow the style of existing archive entries:

```yaml
- name: <name>
remote: <remote> # use an existing remote if its url-base is a prefix of the URL; omit otherwise
vcs: archive
src: <path-in-archive> # omit if copying from the archive root
dst: <dst>
repo-path: <url-or-path>
ignore:
- <pattern>
integrity:
hash: sha256:<hash>
```

Compute the hash before writing the entry:

```bash
curl -sL <url> | sha256sum
```

When reusing an existing remote, `repo-path` is the URL suffix after the remote's `url-base`. When no remote matches, omit `remote:` and use the full URL as `repo-path`.

## Step 5 — Fetch and verify

Run `dfetch update <name>`. If it fails:

| Error | Fix |
|---|---|
| `src … not found in archive` | Inspect the archive with `unzip -l <file>` or `tar -tf <file>`. If the archive has a single top-level wrapper directory, dfetch strips it — adjust `src` accordingly. |
| Integrity mismatch | Recompute the hash and update the `integrity` field. |
| Remote not found | Check that `remote:` matches a name in the `remotes:` list. |
| Branch/tag not found | Run `dfetch add -i <url>` in a terminal to browse available versions interactively, then copy the chosen value back into the manifest. |

## Step 6 — Confirm

Show the user the new entry that was added to `dfetch.yaml` and list the files that were fetched to `dst`.
11 changes: 9 additions & 2 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"Bash(codespell)",
"Bash(python -m behave *)",
"Bash(python -m pytest tests/*)",
"Bash(pre-commit run:*)",
"Bash(pre-commit run:*)",
"Bash(git stash:*)",
"Bash(xenon *)",
"Bash(radon *)",
Expand All @@ -21,7 +21,14 @@
"Bash(xargs pyupgrade:*)",
"Bash(lint-imports)",
"Bash(pip install:*)",
"Bash(pytest tests/test_sbom_reporter.py -q)"
"Bash(pytest tests/test_sbom_reporter.py -q)",
"Bash(make -C doc latexpdf)",
"Bash(make -C doc clean)",
"Bash(dfetch add:*)",
"Bash(dfetch update:*)"
],
"additionalDirectories": [
"/workspaces/dfetch/.claude"
]
}
}
3 changes: 3 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@ RUN apt-get update && apt-get install --no-install-recommends -y \

# Install LaTeX for PDF documentation generation
# texlive packages follow the TeX Live release cycle and are not pinned
# texlive-xetex: xelatex engine (required by latex_engine = "xelatex" in conf.py)
# texlive-latex-extra: pulls in texlive-pictures (TikZ) as a dependency
RUN apt-get update && apt-get install --no-install-recommends -y \
texlive-latex-recommended \
texlive-fonts-recommended \
texlive-latex-extra \
texlive-xetex \
latexmk && \
rm -rf /var/lib/apt/lists/*

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ jobs:
- name: Install LaTeX
run: |
sudo apt-get install -y texlive-latex-recommended texlive-fonts-recommended \
texlive-latex-extra latexmk
texlive-latex-extra texlive-xetex latexmk

Comment thread
spoorcc marked this conversation as resolved.
- name: Build PDF
run: make -C doc latexpdf
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ Release 0.14.0 (unreleased)
* Embed base64-encoded license text in SBOM ``licenses[].text`` when a license is successfully identified (#1112)
* Set SBOM ``licenses`` to the SPDX expression ``NOASSERTION`` when a license file is not found or cannot be classified (#1112)
* Add a ``dfetch:license:finding`` property to SBOM when ``NOASSERTION`` is set, explaining the reason (#1112)
* Add ``dfetch:license:<spdx-id>:confidence``, ``dfetch:license:threshold``, and ``dfetch:license:tool`` SBOM properties (#1116)
* Add ``dfetch:license:threshold`` and ``dfetch:license:tool`` SBOM properties (#1116)
* Add ``dfetch:license:<spdx-id>:confidence`` SBOM property for per-licence confidence scores (#1116)
* Use github purl, repo and version for a github release archive in SBOM (#1063)
* Allow ``dfetch freeze`` to accept project names to freeze only specific projects (#1063)
* Edit manifest in-place when freezing inside a git or SVN superproject, preserving comments and layout (#1063)
Expand Down
30 changes: 30 additions & 0 deletions dfetch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ manifest:
- name: github
url-base: https://github.com/

- name: ctan
url-base: https://mirrors.ctan.org/

projects:
- name: demo-magic
repo-path: paxtonhare/demo-magic.git
Expand Down Expand Up @@ -67,3 +70,30 @@ manifest:
- metadata
integrity:
hash: sha256:218d19fdec1bd898d1c78683f3c72e71bcc9e5f9bb3e065f99a5c3cdc48e0d66

- name: poiret-one-font
remote: github
src: fonts
dst: doc/static/fonts/poiretone
branch: master
repo-path: alexeiva/poiretone.git
ignore:
- otf

- name: tex-gyre-heros-font
Comment thread
spoorcc marked this conversation as resolved.
Dismissed
remote: ctan
vcs: archive
src: opentype/
dst: doc/static/fonts/texgyreheros
repo-path: fonts/tex-gyre.zip
ignore:
- texgyreadventor*
- texgyrebonum*
- texgyrechorus*
- texgyrecursor*
- texgyreheroscn*
- texgyrepagella*
- texgyreschola*
- texgyretermes*
integrity:
hash: sha256:1773c470f9e388e087b68e3426e115af2cd236845a7e05ceb25b2a503409a7a3
66 changes: 66 additions & 0 deletions doc/_ext/unique_section_ids.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Sphinx extension that makes section IDs unique across a document.

sphinx-argparse generates identical section IDs (e.g.
``dfetch.__main__-create_parser-positional-arguments``) for every subcommand
because they all share the same argparse function. The extension's own
``ensure_unique_ids`` helper only deduplicates within a single directive call,
so IDs remain duplicate across subcommand blocks. These duplicates produce
``multiply-defined label`` warnings in the LaTeX output.

This extension runs a second deduplication pass over the fully-resolved
doctree, appending ``_repeat1``, ``_repeat2``, … suffixes to any duplicate IDs
that survive after sphinx-argparse's own pass.

Register in ``conf.py``::

extensions = [..., "unique_section_ids"]
"""

from typing import Any

from docutils import nodes
from sphinx.application import Sphinx


def _deduplicate_ids(_app: Sphinx, doctree: nodes.document, _fromdocname: str) -> None:
"""Rename duplicate section IDs within a resolved doctree.

Traverses every ``section`` node in *doctree* and ensures each ID is
unique. When a collision is detected the duplicate receives a
``_repeat<n>`` suffix (incrementing *n* until the result is free), which
mirrors the strategy used by sphinx-argparse's own ``ensure_unique_ids``
helper but applies it document-wide rather than per-directive.

Args:
_app: The Sphinx application object (unused).
doctree: The fully-resolved document tree being processed.
_fromdocname: The document name that produced the tree (unused).
"""
seen: set[str] = set()
for node in doctree.traverse(nodes.section):
new_ids: list[str] = []
for id_ in node["ids"]:
if id_ not in seen:
seen.add(id_)
new_ids.append(id_)
else:
counter = 1
while f"{id_}_repeat{counter}" in seen:
counter += 1
unique_id = f"{id_}_repeat{counter}"
seen.add(unique_id)
new_ids.append(unique_id)
node["ids"] = new_ids


def setup(app: Sphinx) -> dict[str, Any]:
"""Register the section-ID deduplication handler with Sphinx.

Args:
app: The Sphinx application object.

Returns:
Extension metadata dictionary.
"""
app.connect("doctree-resolved", _deduplicate_ids)
return {"version": "0.1", "parallel_read_safe": True}
69 changes: 39 additions & 30 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"sphinx_design",
"plantweb.directive",
"scenario_directive",
"unique_section_ids",
"sphinx.ext.autodoc",
"sphinx.ext.autosectionlabel",
"sphinx.ext.napoleon",
Expand Down Expand Up @@ -110,6 +111,7 @@
# Suppress warnings about duplicate labels from argparse directive
suppress_warnings = [
"autosectionlabel.reference/commands",
"autosectionlabel.reference/manifest",
"autosectionlabel.howto/updating-projects",
]

Expand Down Expand Up @@ -191,6 +193,9 @@

# -- Options for LaTeX output ---------------------------------------------

latex_engine = "xelatex"
latex_use_xindy = False

latex_logo = "images/dfetch_logo.png"

latex_elements = {
Expand All @@ -202,14 +207,33 @@
\usepackage{pifont}
\newunicodechar{✔}{\ding{51}}
\newunicodechar{✘}{\ding{55}}
\usepackage{helvet}
\renewcommand*\familydefault{\sfdefault}
\usepackage[T1]{fontenc}
\usepackage{xcolor}
\definecolor{dfprimary}{HTML}{c2620a}
\definecolor{dfaccent}{HTML}{4e7fa0}
\definecolor{dftextmuted}{HTML}{78716c}
\definecolor{dfnearblack}{HTML}{1c1917}
% Cover page colours (dfetch brand palette)
\definecolor{dfetchCoverTop}{HTML}{3a6682}
\definecolor{dfetchCoverDark}{HTML}{1c1917}
\definecolor{dfetchCoverBottom}{HTML}{c2620a}
\definecolor{dfetchCoverAccent}{HTML}{4e7fa0}
\definecolor{dfetchCoverLight}{HTML}{fef8f0}
% TikZ for cover page
\usepackage{tikz}
""",
# XeLaTeX font setup using vendored fonts (see dfetch.yaml).
# Both fonts are copied into the LaTeX build root via latex_additional_files,
# so Path=./ resolves correctly at compile time.
# \PoiretOne is declared here so it is available to the cover page.
"fontpkg": r"""
\usepackage{fontspec}
\setsansfont{texgyreheros-regular}[
Extension=.otf, Path=./, Scale=0.95,
BoldFont=texgyreheros-bold,
ItalicFont=texgyreheros-italic,
BoldItalicFont=texgyreheros-bolditalic]
\renewcommand*\familydefault{\sfdefault}
\newfontface\PoiretOne{PoiretOne-Regular}[Extension=.ttf, Path=./]
""",
# Design-token colours for Sphinx's built-in LaTeX style hooks
"sphinxsetup": (
Expand All @@ -221,33 +245,9 @@
"noteBorderColor={rgb}{0.306,0.498,0.627},"
"warningBorderColor={rgb}{0.761,0.384,0.039},"
),
# Custom title page with amber header bar, logo, and accent footer.
# \makeatletter/\makeatother are required to access \py@release (@ is a
# letter in LaTeX package code but not in regular document mode).
# \sphinxlogo is NOT used here because it has no size constraint; instead
# we include the logo directly with an explicit width to keep the page count
# at exactly one regardless of the image's natural resolution.
"maketitle": r"""
\makeatletter
\begin{titlepage}
\noindent{\color{dfprimary}\rule{\linewidth}{6pt}}\par
\vspace*{\fill}
\begin{center}
\includegraphics[width=0.35\linewidth]{dfetch_logo.png}\par
\vspace{1.2cm}
{\fontsize{40}{44}\selectfont\bfseries\color{dfprimary}Dfetch\par}
\vspace{0.3cm}
{\LARGE\color{dfnearblack}Documentation\par}
\vspace{0.6cm}
{\large\color{dftextmuted}\textit{vendor dependencies without the pain}\par}
\vspace{1.5cm}
{\large\color{dftextmuted}\py@release\par}
\end{center}
\vspace*{\fill}
\noindent{\color{dfaccent}\rule{\linewidth}{4pt}}\par
\end{titlepage}
\makeatother
""",
# Cover page is in doc/dfetch_cover.inc (listed in latex_additional_files).
# \makeatletter/\makeatother expose \py@release inside the included file.
"maketitle": r"\makeatletter\input{dfetch_cover.inc}\makeatother",
}

# Grouping the document tree into LaTeX files. List of tuples
Expand All @@ -263,6 +263,15 @@
),
]

latex_additional_files = [
"dfetch_cover.inc",
"static/fonts/poiretone/ttf/PoiretOne-Regular.ttf",
"static/fonts/texgyreheros/texgyreheros-regular.otf",
"static/fonts/texgyreheros/texgyreheros-bold.otf",
"static/fonts/texgyreheros/texgyreheros-italic.otf",
"static/fonts/texgyreheros/texgyreheros-bolditalic.otf",
]


# -- Options for manual page output ---------------------------------------

Expand Down
Loading
Loading