Add MIT license, full README and CLAUDE.md

- LICENSE: MIT, referenced from pyproject.toml metadata.
- README: explains what the project is, how to install conandeps with uv,
  every option, exit codes, a worked example with a conflict/override
  walkthrough, how it works internally, and the container dev workflow.
- CLAUDE.md: hard constraints (Conan 1.66 only, Python <3.12, all work in
  the container, private remote "knor" never available in tests) and the
  design decisions behind conandeps so future sessions do not revisit them.
- Ignore uv build output (dist/).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYNdaGDrpaqDyT8QtrTQbM
This commit is contained in:
Ole-Morten Duesund 2026-08-25 14:56:00 +02:00
commit 215925b480
5 changed files with 236 additions and 42 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@ __pycache__/
.pytest_cache/ .pytest_cache/
.ruff_cache/ .ruff_cache/
*.html *.html
dist/

78
CLAUDE.md Normal file
View file

@ -0,0 +1,78 @@
# CLAUDE.md
Guidance for working on this repository. It records decisions made when the
project was started (August 2026) that are not obvious from the code.
## What this is
Small single-file Python utilities for **Conan 1.x, targeting 1.66**. Not
Conan 2. The first (and so far only) tool is `conandeps.py`; see `README.md`
for what it does and how it is used.
## Hard constraints
- **Conan 1.66 only.** Use the `conans.client.conan_api.ConanAPIV1` API and
Conan 1 graph internals (`Node.binary`, `Node.dependencies`,
`conanfile.requires`). Do not introduce Conan 2 APIs or a Conan 2 code path.
- **Python 3.83.11.** Conan 1.x imports the `imp` module, which is gone in
3.12. `requires-python` in `pyproject.toml` and the PEP 723 header in
`conandeps.py` both say `<3.12`; keep them in sync.
- **All development runs inside the container.** Use `./dev.sh <cmd>` for
every python/pytest/ruff/uv invocation. Never run `conan` against the
host's `~/.conan`; never `pip install` on the host. Rebuild the image with
`./dev.sh build` after touching `Containerfile`.
- **Never depend on the private remote.** The real remote is called `knor`
and only exists on the machine where the tool is used. Tests must use the
local `conan_server` fixture in `tests/test_conandeps.py`.
## Design decisions (and why)
- **Binary availability is computed via Conan's graph, not `conan search`.**
The graph is built once per build type through `ConanAPIV1.info()` and each
node's `binary` status is read. This honours `package_id()` overrides,
options and `default_package_id_mode` exactly like `conan install`. Do not
replace this with settings-dict matching against `conan search` output.
- **"Missing" means missing for the exact profile.** Not "no binary with
that build_type at all".
- **Overrides are parsed from Conan's WARN output.** Conan 1 does not record
overrides on the graph; `Requirements.update()` mutates `req.ref` in place
and only emits `"<pkg>: requirement <old> overridden by <who> to <new>"`.
`conandeps` captures the output stream with a non-coloured `ConanOutput`
and matches `_OVERRIDE_RE`. `test_overrides` pins this format if a Conan
patch release changes the wording, that test is the alarm.
- **Both explicit and implicit overrides are reported.** `override=True`
requirements are collected from `conanfile.requires` to label an override
as explicit; everything else parsed from the WARN lines is implicit.
- **`range_ref` is not a version range after an override.** Conan reuses
`Requirement.range_ref` to hold the pre-override reference, so only treat
it as a range when `req.version_range` is truthy.
- **Single file, stdlib + Conan only.** No third-party deps beyond Conan.
Packaging is hatchling with `only-include = ["conandeps.py"]`; the PEP 723
header makes the file usable via `uv run conandeps.py` without a checkout.
- **Exit codes:** `0` fine, `1` missing binaries, `2` Conan error. CI relies
on this.
## Workflow
- Lint/format: `./dev.sh ruff check .` and `./dev.sh ruff format .`
(config in `pyproject.toml`, target py38, line length 100). Run
`shellcheck dev.sh` after editing the wrapper.
- Tests: `./dev.sh python -m pytest`. The fixture spins up `conan_server`
on a free port with a pre-written `server.conf` (the port cannot be given
on the command line), creates fake packages whose `build()` does nothing,
uploads them, then wipes the cache so binaries can only come from the
remote. Fake recipes need `--build=missing` at create time because
dependencies' binaries are deliberately incomplete.
- Dependencies: `uv.lock` is committed; regenerate with `./dev.sh uv lock`
after changing `pyproject.toml`.
- Commits: atomic, no `--amend`; run ruff + pytest + shellcheck before
committing. Add a memory of anything a future session could not derive
from the repository.
## Adding another tool
Follow the `conandeps.py` pattern: one executable file with a module
docstring explaining *how* and *why*, a PEP 723 header, a `main(argv)` that
returns an exit code, a `[project.scripts]` entry, an `only-include` entry in
`pyproject.toml`, tests under `tests/` using the shared `conan_env` fixture,
and a section in `README.md`.

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Ole-Morten Duesund
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

178
README.md
View file

@ -1,74 +1,164 @@
# conan-utils # conan-utils
Small utilities for **Conan 1.x** (targets 1.66). All development happens inside Small, single-file utilities for **Conan 1.x** (targeting 1.66) that answer
a pinned Podman container so nothing from the host environment leaks in. questions the stock `conan info` / `conan search` commands make hard to answer.
Development and testing happen inside a pinned Podman container so nothing from
the host environment leaks in.
## conandeps.py Licensed under the [MIT License](LICENSE).
Given a `conanfile.py`, `conandeps` reports: ## Tools
* direct and indirect dependencies (host and build context), | Tool | What it does |
* whether a binary exists on the remote for **Release, Debug and RelWithDebInfo** |------|--------------|
(configurable) for *your exact profile*, | [`conandeps`](#conandeps) | Lists direct/indirect dependencies, checks which build types have binaries on a remote for your exact profile, and shows requirement overrides. |
* every requirement **override** explicit `override=True` and implicit ones
(a downstream consumer asking for a newer version than a transitive
dependency declared) shown both as a list and on the edges of the
dependency tree.
### Installing with uv ## conandeps
``` Given a `conanfile.py`, `conandeps` tells you three things in one report:
uv tool install git+https://<your-forge>/conan-utils # or: uv tool install . from a checkout
1. **What you depend on** direct and indirect requirements, host and build
context, as a table and as a tree.
2. **Which binaries are missing** for every dependency, whether a package
exists on the remote for **Release, Debug and RelWithDebInfo** (or any list
you choose), evaluated for *your exact profile* (compiler, version, libcxx,
arch, options, …), not "some binary with that build type".
3. **Which requirements were overridden** both explicit
`self.requires("x/2.0", override=True)` and implicit ones, where a
downstream consumer simply asks for a newer version than a transitive
dependency declared. Each override is listed with who wanted what and who
forced the change, and annotated on the exact edge of the dependency tree
where it happened.
### Installation
Requires Python 3.83.11 (Conan 1.x does not run on 3.12+). With
[uv](https://docs.astral.sh/uv/):
```bash
# As a command on your PATH, in its own venv with Conan 1.66:
uv tool install git+https://<your-forge>/conan-utils # or `uv tool install .` from a checkout
conandeps path/to/conanfile.py -r knor conandeps path/to/conanfile.py -r knor
```
This puts a `conandeps` command on your PATH in its own virtualenv with # Or run the single file directly, no checkout needed (PEP 723 inline metadata):
Conan 1.66 — it does not touch or depend on the Conan you use for builds
(but it does read the same `~/.conan` config, profiles and remotes).
The script also carries PEP 723 inline metadata, so the single file works on
its own without a checkout:
```
uv run conandeps.py path/to/conanfile.py -r knor uv run conandeps.py path/to/conanfile.py -r knor
``` ```
Python 3.83.11 is required; Conan 1.x does not run on 3.12+. The tool's venv carries its own Conan 1.66 and does not touch the Conan you
build with, but it reads the same `~/.conan` configuration: profiles,
`remotes.json` and stored remote credentials. Log in to your remote once with
`conan user -r <remote> -p` (or set `CONAN_LOGIN_USERNAME` / `CONAN_PASSWORD`)
if it requires authentication.
### Usage ### Usage
``` ```
./conandeps.py path/to/conanfile.py -r knor # text report conandeps CONANFILE [-r REMOTE] [-pr PROFILE] [-s KEY=VALUE] [-o PKG:KEY=VALUE]
./conandeps.py path/to/conanfile.py -r knor -pr myprofile # explicit profile [--build-types Release,Debug,RelWithDebInfo] [-u] [--html FILE] [-v]
./conandeps.py path/to/conanfile.py -r knor --html report.html
./conandeps.py path/to/conanfile.py -r knor --build-types Release,Debug
``` ```
Options mirror `conan info`: `-pr/--profile`, `-s/--settings`, `-o/--options` | Option | Meaning |
(all repeatable) and `-u/--update`. Exit status is `1` when any binary is |--------|---------|
missing, so it can gate a CI job. | `CONANFILE` | Path to `conanfile.py` (or a directory containing one). |
| `-r`, `--remote` | Remote to look for binaries in, e.g. `-r knor`. Default: all configured remotes. |
| `-pr`, `--profile` | Profile to evaluate with (repeatable, like `conan -pr`). Default: your default profile. |
| `-s`, `-o` | Extra settings / options, repeatable, same syntax as `conan install`. |
| `--build-types` | Comma-separated build types to check. Default: `Release,Debug,RelWithDebInfo`. |
| `-u`, `--update` | Ask the remote for newer recipes/binaries (`conan -u`). |
| `--html FILE` | Also write a self-contained HTML report (no scripts, no external assets). |
| `-v` | Echo Conan's own output to stderr. |
Exit status: `0` all binaries present, `1` at least one is missing, `2` Conan
failed to build the graph (for example a version conflict). This makes it
usable as a CI gate.
### Example
```
$ conandeps MyProject/conanfile.py -r knor
conandeps report for MyProject/conanfile.py
remote : knor
profile: arch=x86_64, compiler=gcc, compiler.libcxx=libstdc++11, compiler.version=14, os=Linux
Dependencies (4) and binary availability
package kind ctx Release Debug RelWithDebInfo
-------------------------------------------------------------------------
MyDepA/1.0 direct host ok ok ok
boost/1.8 indir. host ok ok ok
MyDepB/1.0 direct host ok ok ok
Zigma/1.0 indir. host ok ok MISSING
MISSING BINARIES (1 packages)
Zigma/1.0: missing RelWithDebInfo
RelWithDebInfo package_id 3f9c...e21a
Overrides (1)
Zigma/1.0 wanted boost/1.7
-> forced to boost/1.8 by your conanfile [implicit (newer direct requirement)]
Dependency hierarchy
MyProject/conanfile.py
|-- MyDepA/1.0
| `-- boost/1.8
`-- MyDepB/1.0
`-- Zigma/1.0 [MISSING: RelWithDebInfo]
`-- boost/1.8 [overrides boost/1.7 (implicit by your conanfile)]
```
Reading the table:
* `ok` a binary for your profile exists (in the remote, or already in your
cache).
* `MISSING` no binary for the package_id your profile produces. Hover the
cell in the HTML report to see the package_id.
* `-` not applicable (Conan marked the node `Skip` or `Editable`).
* Header-only packages show `ok` everywhere: their package_id ignores
`build_type`.
Things worth knowing:
* Two sibling dependencies requiring different versions of the same package
with no decision from your conanfile is a **conflict** in Conan 1.x, not an
override. Conan refuses to build the graph, and `conandeps` prints Conan's
conflict message and exits with `2`.
* An override can change the package_id of the package whose requirement was
rewritten (with the default `semver_direct_mode`, a direct requirement's
version is part of the id). If that makes *all* build types of a package go
`MISSING`, the report is telling the truth: no binary on the remote was built
against the overridden version.
### How it works ### How it works
Rather than matching `conan search` output by hand, the dependency graph is Rather than hand-matching `conan search` output against your profile, the
built once per build type through Conan's own `info` code path and each node's dependency graph is built once per build type through Conan's own `info` API
binary status (`Cache`/`Download`/`Missing`, …) is read back. That means the same code path `conan install` uses and each node's binary status
(`Cache`, `Download`, `Update`, `Missing`, …) is read back. That is why
`package_id()` customisations, options and `default_package_id_mode` are `package_id()` customisations, options and `default_package_id_mode` are
honoured exactly as a real `conan install` would. honoured exactly as a real install would.
Conan 1.x does not keep override information on the graph the only trace is a Conan 1.x does not keep override information on the graph object; the only
`WARN: … requirement A overridden by B to C` line. The tool captures Conan's trace is a `WARN: <pkg>: requirement A overridden by B to C` line written while
output while building the graph and parses those lines. the graph is resolved. The tool captures Conan's output stream during graph
construction and parses those lines. A test pins the message format.
## Development ## Development
``` Everything runs inside the `conan-utils-dev` container (Conan 1.66, Python
./dev.sh build # build the conan-utils-dev image (Conan 1.66, Python 3.11) 3.11, ruff, pytest, uv) via the `dev.sh` wrapper, which mounts the checkout at
./dev.sh python -m pytest # run the tests `/work`:
```bash
./dev.sh build # build the image
./dev.sh python -m pytest # run the tests
./dev.sh ruff check . && ./dev.sh ruff format . ./dev.sh ruff check . && ./dev.sh ruff format .
./dev.sh # interactive shell ./dev.sh # interactive shell
``` ```
The tests start a throw-away `conan_server` inside the container, upload a The tests start a throw-away `conan_server` inside the container, upload a
small graph with deliberately missing binaries and overrides, and run the tool small dependency graph with deliberately missing binaries and both kinds of
against it with an isolated `CONAN_USER_HOME`. override, wipe the local cache, and run the tool against the server using an
isolated `CONAN_USER_HOME`. Your own `~/.conan` is never touched.
See [CLAUDE.md](CLAUDE.md) for the conventions that apply when working on this
repository.

View file

@ -2,6 +2,10 @@
name = "conan-utils" name = "conan-utils"
version = "0.1.0" version = "0.1.0"
description = "Small utilities for Conan 1.x (targets 1.66)" description = "Small utilities for Conan 1.x (targets 1.66)"
readme = "README.md"
license = "MIT"
license-files = ["LICENSE"]
authors = [{ name = "Ole-Morten Duesund" }]
# Conan 1.x imports the `imp` module, which was removed in Python 3.12. # Conan 1.x imports the `imp` module, which was removed in Python 3.12.
requires-python = ">=3.8,<3.12" requires-python = ">=3.8,<3.12"
dependencies = ["conan>=1.66,<2"] dependencies = ["conan>=1.66,<2"]