vibefpmgui.manifest

Reading fpm manifests and resolving the dependency tree (no Qt in here).

The tree is built by starting at the main fpm.toml and following every dependency to its own fpm.toml. Where a dependency lives on disk is taken from (in order of preference):

  1. path = "..." in the dependency spec (relative to the declaring package),
  2. build/cache.toml written by fpm after it resolves the dependencies,
  3. the default fpm download location build/dependencies/<name>.

Dependencies that have not been downloaded yet are still shown, but flagged as missing (run fpm update or fpm build to fetch them).

  1"""Reading fpm manifests and resolving the dependency tree (no Qt in here).
  2
  3The tree is built by starting at the main ``fpm.toml`` and following every
  4dependency to its own ``fpm.toml``.  Where a dependency lives on disk is taken
  5from (in order of preference):
  6
  71. ``path = "..."`` in the dependency spec (relative to the declaring package),
  82. ``build/cache.toml`` written by fpm after it resolves the dependencies,
  93. the default fpm download location ``build/dependencies/<name>``.
 10
 11Dependencies that have not been downloaded yet are still shown, but flagged as
 12missing (run ``fpm update`` or ``fpm build`` to fetch them).
 13"""
 14
 15from __future__ import annotations
 16
 17import re
 18import tomllib
 19from dataclasses import dataclass, field
 20from pathlib import Path
 21
 22# Names that fpm treats as metapackages when given as ``name = "*"``.
 23METAPACKAGES = {
 24    "openmp", "stdlib", "minpack", "mpi", "hdf5", "netcdf", "blas", "lapack",
 25}
 26
 27# Node kinds
 28ROOT = "root"
 29GIT = "git"
 30PATH = "path"
 31REGISTRY = "registry"
 32META = "metapackage"
 33MISSING = "missing"
 34DEV_ONLY = "dev-only"  # a dependency's dev-dependency: fpm doesn't download it (see resolve)
 35
 36
 37@dataclass
 38class Package:
 39    """A node in the dependency graph."""
 40
 41    name: str
 42    kind: str
 43    version: str = ""
 44    manifest_version: str = ""  # the version its own fpm.toml gives ("": none)
 45    directory: Path | None = None
 46    manifest: Path | None = None
 47    license: str = ""
 48    author: str = ""
 49    description: str = ""
 50    homepage: str = ""
 51    git: str = ""
 52    git_ref: str = ""
 53    git_rev: str = ""
 54    error: str = ""
 55    asked_kind: str = ""  # a DEV_ONLY package's: the kind it would be (GIT, PATH, REGISTRY or META)
 56
 57
 58@dataclass
 59class Edge:
 60    parent: str
 61    child: str
 62    dev: bool = False  # from [dev-dependencies] / test or example targets
 63
 64
 65@dataclass
 66class Declaration:
 67    """One place a dependency is declared: which package asks for it, and how."""
 68    parent: str
 69    spec: object       # as written in the parent's fpm.toml
 70    dev: bool = False
 71    parent_dir: Path | None = None  # (for resolving a relative ``path``)
 72
 73
 74@dataclass
 75class DependencyGraph:
 76    root: str
 77    packages: dict[str, Package] = field(default_factory=dict)
 78    edges: list[Edge] = field(default_factory=list)
 79    # Every declaration of each dependency, closest to the root first (the
 80    # first is the one fpm uses).
 81    declarations: dict[str, list[Declaration]] = field(default_factory=dict)
 82
 83    def children(self, name: str) -> list[Edge]:
 84        return [e for e in self.edges if e.parent == name]
 85
 86    def parents(self, name: str) -> list[Edge]:
 87        return [e for e in self.edges if e.child == name]
 88
 89
 90def load_toml(path: Path) -> dict:
 91    with open(path, "rb") as f:
 92        return tomllib.load(f)
 93
 94
 95def _read_version(value, package_dir: Path) -> str:
 96    """fpm allows ``version`` to be a string or the name of a file holding it."""
 97    if not isinstance(value, str):
 98        return ""
 99    candidate = package_dir / value
100    if candidate.is_file():
101        try:
102            return candidate.read_text().strip().splitlines()[0].strip()
103        except (OSError, IndexError):
104            return value
105    return value
106
107
108def _as_text(value) -> str:
109    if isinstance(value, list):
110        return ", ".join(str(v) for v in value)
111    return "" if value is None else str(value)
112
113
114def _collect_deps(data: dict, is_root: bool) -> list[tuple[str, object, bool]]:
115    """Return ``(name, spec, dev)`` for every dependency a manifest declares.
116
117    Dev-dependencies and per-target dependencies are only used by fpm for the
118    root project (``is_root``); for a downloaded package they're left out
119    (``is_root`` False), though `resolve` can ask for them to show them.
120    """
121    deps: dict[str, tuple[object, bool]] = {}
122
123    def add(table, dev):
124        if not isinstance(table, dict):
125            return
126        for name, spec in table.items():
127            # A normal dependency wins over a dev one of the same name.
128            if name not in deps or (deps[name][1] and not dev):
129                deps[name] = (spec, dev)
130
131    add(data.get("dependencies"), False)
132    if is_root:
133        add(data.get("dev-dependencies"), True)
134        for target in data.get("executable", []) or []:
135            add(target.get("dependencies"), False)
136        for section in ("test", "example"):
137            for target in data.get(section, []) or []:
138                add(target.get("dependencies"), True)
139    return [(name, spec, dev) for name, (spec, dev) in deps.items()]
140
141
142def _fill_from_manifest(pkg: Package, data: dict) -> None:
143    if pkg.directory is not None:
144        pkg.manifest_version = _read_version(data.get("version"), pkg.directory)
145        pkg.version = pkg.manifest_version or pkg.version
146    pkg.license = _as_text(data.get("license"))
147    pkg.author = _as_text(data.get("author"))
148    pkg.description = _as_text(data.get("description"))
149    pkg.homepage = _as_text(data.get("homepage"))
150
151
152def resolve(manifest_path: str | Path, dependencies_dev: bool = False) -> DependencyGraph:
153    """Build the dependency graph for the project whose manifest is given.
154
155    ``dependencies_dev``: also the dependencies' own dev-dependencies (and
156    their test and example targets' dependencies), as dev edges. fpm uses
157    only the project's own, so it never downloads these: one that isn't
158    also a dependency fpm does use is a `DEV_ONLY` package (no folder or
159    manifest), and their declarations aren't the graph's (`conflicts` is
160    about what fpm uses).
161
162    Raises ``OSError``/``tomllib.TOMLDecodeError`` if the main manifest can't
163    be read; problems with dependency manifests are recorded on the nodes.
164    """
165    manifest_path = Path(manifest_path).resolve()
166    root_dir = manifest_path.parent
167    data = load_toml(manifest_path)
168
169    root_name = str(data.get("name", root_dir.name))
170    graph = DependencyGraph(root=root_name)
171    root = Package(root_name, ROOT, directory=root_dir, manifest=manifest_path)
172    _fill_from_manifest(root, data)
173    graph.packages[root_name] = root
174
175    cache: dict = {}
176    cache_file = root_dir / "build" / "cache.toml"
177    if cache_file.is_file():
178        try:
179            cache = load_toml(cache_file)
180        except (OSError, tomllib.TOMLDecodeError):
181            cache = {}
182
183    # Breadth-first so the first spec seen for a package (closest to the
184    # root) is the one used, which matches how fpm resolves duplicates.
185    queue: list[tuple[Package, dict]] = [(root, data)]
186    unused: list[tuple[str, str, object]] = []  # (parent, name, spec) of the dependencies' dev-dependencies
187    while queue:
188        parent, parent_data = queue.pop(0)
189        is_root = parent is root
190        if dependencies_dev and not is_root:
191            used = {name for name, _spec, _dev in _collect_deps(parent_data, False)}
192            unused += [(parent.name, name, spec) for name, spec, _dev in _collect_deps(parent_data, True)
193                       if name not in used]
194        for name, spec, dev in _collect_deps(parent_data, is_root):
195            graph.edges.append(Edge(parent.name, name, dev))
196            graph.declarations.setdefault(name, []).append(
197                Declaration(parent.name, spec, dev, parent.directory or root_dir))
198            if name in graph.packages:
199                continue
200            pkg, pkg_data = _locate(name, spec, parent, root_dir, cache.get(name, {}))
201            graph.packages[name] = pkg
202            if pkg_data is not None:
203                queue.append((pkg, pkg_data))
204    # After every dependency fpm uses is in: a dev-dependency that's also one
205    # of those is that package (downloaded); only the others are DEV_ONLY.
206    for parent_name, name, spec in unused:
207        graph.edges.append(Edge(parent_name, name, True))
208        if name not in graph.packages:
209            graph.packages[name] = _dev_only(name, spec, parent_name)
210    return graph
211
212
213def _dev_only(name: str, spec, parent: str) -> Package:
214    """A dependency's dev-dependency fpm doesn't download: what its manifest
215    asks for (no folder, no manifest)."""
216    pkg = Package(name, DEV_ONLY, error=f"A dev-dependency of {parent}: fpm doesn't download a dependency's "
217                                        "dev-dependencies (only the project's own).")
218    if isinstance(spec, str):
219        pkg.version = "" if spec == "*" else spec
220        pkg.asked_kind = META if name in METAPACKAGES else REGISTRY
221    elif isinstance(spec, dict):
222        pkg.asked_kind = PATH if "path" in spec else GIT if "git" in spec else REGISTRY
223        pkg.git = str(spec.get("git", ""))
224        for key in ("tag", "branch", "rev"):
225            if key in spec:
226                pkg.git_ref = f"{key} {spec[key]}"
227                break
228        pkg.version = str(spec.get("v", ""))
229    return pkg
230
231
232def _locate(name, spec, parent: Package, root_dir: Path, cached: dict):
233    """Create the Package for a dependency and load its manifest if present."""
234    if isinstance(spec, str):
235        # ``openmp = "*"`` style metapackage (or a bare registry version).
236        kind = META if name in METAPACKAGES else REGISTRY
237        return Package(name, kind, version="" if spec == "*" else spec), None
238    if not isinstance(spec, dict):
239        return Package(name, MISSING, error=f"Unrecognised dependency spec: {spec!r}"), None
240
241    pkg = Package(name, GIT)
242    directory: Path | None = None
243    if "path" in spec:
244        pkg.kind = PATH
245        directory = ((parent.directory or root_dir) / str(spec["path"])).resolve()
246    else:
247        if "git" in spec:
248            pkg.git = str(spec["git"])
249            for key in ("tag", "branch", "rev"):
250                if key in spec:
251                    pkg.git_ref = f"{key} {spec[key]}"
252                    break
253        else:
254            pkg.kind = REGISTRY
255            pkg.version = str(spec.get("v", ""))
256        if cached.get("proj-dir"):
257            directory = (root_dir / str(cached["proj-dir"])).resolve()
258        else:
259            directory = root_dir / "build" / "dependencies" / name
260        pkg.git = pkg.git or str(cached.get("git", ""))
261        # The commit actually checked out; build/cache.toml's "rev" can be
262        # stale (fpm update leaves the old one there).
263        pkg.git_rev = checked_out_commit(directory) or str(cached.get("rev", ""))
264    # (fpm writes "0" for a package whose manifest gives no version: none, then.)
265    if cached.get("version") and str(cached["version"]) != "0" and not pkg.version:
266        pkg.version = str(cached["version"])
267
268    manifest = directory / "fpm.toml"
269    if not manifest.is_file():
270        pkg.directory = directory
271        if pkg.kind == PATH:
272            pkg.error = f"No fpm.toml found in {directory}"
273        else:
274            pkg.error = "Not downloaded yet (run fpm update or fpm build)."
275        pkg.kind = MISSING
276        return pkg, None
277
278    pkg.directory = directory
279    pkg.manifest = manifest
280    try:
281        data = load_toml(manifest)
282    except (OSError, tomllib.TOMLDecodeError) as exc:
283        pkg.error = f"Could not read {manifest}: {exc}"
284        return pkg, None
285    _fill_from_manifest(pkg, data)
286    return pkg, data
287
288
289def checked_out_commit(directory: Path) -> str:
290    """The commit a git checkout is at, read from .git/HEAD (no git needed);
291    "" if it isn't a git checkout."""
292    git = directory / ".git"
293    try:
294        head = (git / "HEAD").read_text().strip()
295    except OSError:
296        return ""
297    if not head.startswith("ref:"):
298        return head if re.fullmatch(r"[0-9a-f]{40,64}", head) else ""
299    ref = head[4:].strip()
300    try:
301        return (git / ref).read_text().strip()
302    except OSError:
303        pass
304    try:  # packed refs: "<sha> <ref>" lines
305        for line in (git / "packed-refs").read_text().splitlines():
306            sha, _, name = line.partition(" ")
307            if name.strip() == ref:
308                return sha
309    except OSError:
310        pass
311    return ""
312
313
314def project_features(data: dict) -> dict[str, list[str]]:
315    """Features declared in a manifest's ``[features]`` table, each with the
316    (dotted) properties it sets, e.g. ``{"mpi": ["dependencies.mpi", "flags"]}``."""
317    table = data.get("features")
318    if not isinstance(table, dict):
319        return {}
320
321    def paths(value, prefix=""):
322        if isinstance(value, dict):
323            out = []
324            for key, sub in value.items():
325                out += paths(sub, f"{prefix}.{key}" if prefix else str(key))
326            return out
327        return [prefix]
328
329    return {str(name): paths(value) for name, value in table.items()}
330
331
332def project_profiles(data: dict) -> dict[str, list[str]]:
333    """Feature profiles declared in ``[profiles]`` (name -> list of features)."""
334    table = data.get("profiles")
335    if not isinstance(table, dict):
336        return {}
337    return {str(name): [str(f) for f in value] if isinstance(value, list) else []
338            for name, value in table.items()}
339
340
341# ------------------------------------------------------------ conflicts
342def _git_url(url: str) -> str:
343    """A git URL in a form where the same repository compares equal."""
344    url = url.strip().rstrip("/").lower()
345    url = re.sub(r"\.git$", "", url)
346    url = re.sub(r"^git@([^:]+):", r"https://\1/", url)   # git@github.com:a/b -> https://github.com/a/b
347    return re.sub(r"^(https?|git|ssh)://(www\.)?", "", url)
348
349
350def spec_key(declaration: Declaration) -> tuple:
351    """What a declaration asks for, so that different ways of writing the
352    same thing compare equal."""
353    spec = declaration.spec
354    if isinstance(spec, str):
355        return ("version", spec.strip())
356    if not isinstance(spec, dict):
357        return ("other", repr(spec))
358    if "path" in spec:
359        return ("path", str(((declaration.parent_dir or Path()) / str(spec["path"])).resolve()))
360    if "git" in spec:
361        for key in ("tag", "branch", "rev"):
362            if key in spec:
363                value = str(spec[key]).strip()
364                # A commit may be written short or long: compare 7 characters.
365                return ("git", _git_url(str(spec["git"])), key, value[:7] if key == "rev" else value)
366        return ("git", _git_url(str(spec["git"])), "", "")
367    return ("registry", *sorted((str(k), str(v)) for k, v in spec.items()))
368
369
370def describe_spec(spec) -> str:
371    """A declaration in a few words: "tag v0.5.0", "path ../lib", "any version"…"""
372    if isinstance(spec, str):
373        return "any version" if spec.strip() == "*" else f"version {spec}"
374    if not isinstance(spec, dict):
375        return repr(spec)
376    if "path" in spec:
377        return f"path {spec['path']}"
378    if "git" in spec:
379        for key in ("tag", "branch", "rev"):
380            if key in spec:
381                value = str(spec[key])
382                return f"{key} {value[:7] if key == 'rev' else value} of {spec['git']}"
383        return f"default branch of {spec['git']}"
384    if "v" in spec:
385        return f"version {spec['v']}"
386    return ", ".join(f"{k} {v}" for k, v in spec.items())
387
388
389def conflicts(graph: DependencyGraph) -> dict[str, list[Declaration]]:
390    """Dependencies that different packages ask for differently (another
391    tag, commit, branch, repository or path). fpm uses only the declaration
392    closest to the project, silently: these are worth knowing about."""
393    out = {}
394    for name, declarations in graph.declarations.items():
395        if len({spec_key(d) for d in declarations}) > 1:
396            out[name] = declarations
397    return out
METAPACKAGES = {'blas', 'hdf5', 'mpi', 'minpack', 'openmp', 'lapack', 'netcdf', 'stdlib'}
ROOT = 'root'
GIT = 'git'
PATH = 'path'
REGISTRY = 'registry'
META = 'metapackage'
MISSING = 'missing'
DEV_ONLY = 'dev-only'
@dataclass
class Package:
38@dataclass
39class Package:
40    """A node in the dependency graph."""
41
42    name: str
43    kind: str
44    version: str = ""
45    manifest_version: str = ""  # the version its own fpm.toml gives ("": none)
46    directory: Path | None = None
47    manifest: Path | None = None
48    license: str = ""
49    author: str = ""
50    description: str = ""
51    homepage: str = ""
52    git: str = ""
53    git_ref: str = ""
54    git_rev: str = ""
55    error: str = ""
56    asked_kind: str = ""  # a DEV_ONLY package's: the kind it would be (GIT, PATH, REGISTRY or META)

A node in the dependency graph.

Package( name: str, kind: str, version: str = '', manifest_version: str = '', directory: pathlib.Path | None = None, manifest: pathlib.Path | None = None, license: str = '', author: str = '', description: str = '', homepage: str = '', git: str = '', git_ref: str = '', git_rev: str = '', error: str = '', asked_kind: str = '')
name: str
kind: str
version: str = ''
manifest_version: str = ''
directory: pathlib.Path | None = None
manifest: pathlib.Path | None = None
license: str = ''
author: str = ''
description: str = ''
homepage: str = ''
git: str = ''
git_ref: str = ''
git_rev: str = ''
error: str = ''
asked_kind: str = ''
@dataclass
class Edge:
59@dataclass
60class Edge:
61    parent: str
62    child: str
63    dev: bool = False  # from [dev-dependencies] / test or example targets
Edge(parent: str, child: str, dev: bool = False)
parent: str
child: str
dev: bool = False
@dataclass
class Declaration:
66@dataclass
67class Declaration:
68    """One place a dependency is declared: which package asks for it, and how."""
69    parent: str
70    spec: object       # as written in the parent's fpm.toml
71    dev: bool = False
72    parent_dir: Path | None = None  # (for resolving a relative ``path``)

One place a dependency is declared: which package asks for it, and how.

Declaration( parent: str, spec: object, dev: bool = False, parent_dir: pathlib.Path | None = None)
parent: str
spec: object
dev: bool = False
parent_dir: pathlib.Path | None = None
@dataclass
class DependencyGraph:
75@dataclass
76class DependencyGraph:
77    root: str
78    packages: dict[str, Package] = field(default_factory=dict)
79    edges: list[Edge] = field(default_factory=list)
80    # Every declaration of each dependency, closest to the root first (the
81    # first is the one fpm uses).
82    declarations: dict[str, list[Declaration]] = field(default_factory=dict)
83
84    def children(self, name: str) -> list[Edge]:
85        return [e for e in self.edges if e.parent == name]
86
87    def parents(self, name: str) -> list[Edge]:
88        return [e for e in self.edges if e.child == name]
DependencyGraph( root: str, packages: dict[str, Package] = <factory>, edges: list[Edge] = <factory>, declarations: dict[str, list[Declaration]] = <factory>)
root: str
packages: dict[str, Package]
edges: list[Edge]
declarations: dict[str, list[Declaration]]
def children(self, name: str) -> list[Edge]:
84    def children(self, name: str) -> list[Edge]:
85        return [e for e in self.edges if e.parent == name]
def parents(self, name: str) -> list[Edge]:
87    def parents(self, name: str) -> list[Edge]:
88        return [e for e in self.edges if e.child == name]
def load_toml(path: pathlib.Path) -> dict:
91def load_toml(path: Path) -> dict:
92    with open(path, "rb") as f:
93        return tomllib.load(f)
def resolve( manifest_path: str | pathlib.Path, dependencies_dev: bool = False) -> DependencyGraph:
153def resolve(manifest_path: str | Path, dependencies_dev: bool = False) -> DependencyGraph:
154    """Build the dependency graph for the project whose manifest is given.
155
156    ``dependencies_dev``: also the dependencies' own dev-dependencies (and
157    their test and example targets' dependencies), as dev edges. fpm uses
158    only the project's own, so it never downloads these: one that isn't
159    also a dependency fpm does use is a `DEV_ONLY` package (no folder or
160    manifest), and their declarations aren't the graph's (`conflicts` is
161    about what fpm uses).
162
163    Raises ``OSError``/``tomllib.TOMLDecodeError`` if the main manifest can't
164    be read; problems with dependency manifests are recorded on the nodes.
165    """
166    manifest_path = Path(manifest_path).resolve()
167    root_dir = manifest_path.parent
168    data = load_toml(manifest_path)
169
170    root_name = str(data.get("name", root_dir.name))
171    graph = DependencyGraph(root=root_name)
172    root = Package(root_name, ROOT, directory=root_dir, manifest=manifest_path)
173    _fill_from_manifest(root, data)
174    graph.packages[root_name] = root
175
176    cache: dict = {}
177    cache_file = root_dir / "build" / "cache.toml"
178    if cache_file.is_file():
179        try:
180            cache = load_toml(cache_file)
181        except (OSError, tomllib.TOMLDecodeError):
182            cache = {}
183
184    # Breadth-first so the first spec seen for a package (closest to the
185    # root) is the one used, which matches how fpm resolves duplicates.
186    queue: list[tuple[Package, dict]] = [(root, data)]
187    unused: list[tuple[str, str, object]] = []  # (parent, name, spec) of the dependencies' dev-dependencies
188    while queue:
189        parent, parent_data = queue.pop(0)
190        is_root = parent is root
191        if dependencies_dev and not is_root:
192            used = {name for name, _spec, _dev in _collect_deps(parent_data, False)}
193            unused += [(parent.name, name, spec) for name, spec, _dev in _collect_deps(parent_data, True)
194                       if name not in used]
195        for name, spec, dev in _collect_deps(parent_data, is_root):
196            graph.edges.append(Edge(parent.name, name, dev))
197            graph.declarations.setdefault(name, []).append(
198                Declaration(parent.name, spec, dev, parent.directory or root_dir))
199            if name in graph.packages:
200                continue
201            pkg, pkg_data = _locate(name, spec, parent, root_dir, cache.get(name, {}))
202            graph.packages[name] = pkg
203            if pkg_data is not None:
204                queue.append((pkg, pkg_data))
205    # After every dependency fpm uses is in: a dev-dependency that's also one
206    # of those is that package (downloaded); only the others are DEV_ONLY.
207    for parent_name, name, spec in unused:
208        graph.edges.append(Edge(parent_name, name, True))
209        if name not in graph.packages:
210            graph.packages[name] = _dev_only(name, spec, parent_name)
211    return graph

Build the dependency graph for the project whose manifest is given.

dependencies_dev: also the dependencies' own dev-dependencies (and their test and example targets' dependencies), as dev edges. fpm uses only the project's own, so it never downloads these: one that isn't also a dependency fpm does use is a DEV_ONLY package (no folder or manifest), and their declarations aren't the graph's (conflicts is about what fpm uses).

Raises OSError/tomllib.TOMLDecodeError if the main manifest can't be read; problems with dependency manifests are recorded on the nodes.

def checked_out_commit(directory: pathlib.Path) -> str:
290def checked_out_commit(directory: Path) -> str:
291    """The commit a git checkout is at, read from .git/HEAD (no git needed);
292    "" if it isn't a git checkout."""
293    git = directory / ".git"
294    try:
295        head = (git / "HEAD").read_text().strip()
296    except OSError:
297        return ""
298    if not head.startswith("ref:"):
299        return head if re.fullmatch(r"[0-9a-f]{40,64}", head) else ""
300    ref = head[4:].strip()
301    try:
302        return (git / ref).read_text().strip()
303    except OSError:
304        pass
305    try:  # packed refs: "<sha> <ref>" lines
306        for line in (git / "packed-refs").read_text().splitlines():
307            sha, _, name = line.partition(" ")
308            if name.strip() == ref:
309                return sha
310    except OSError:
311        pass
312    return ""

The commit a git checkout is at, read from .git/HEAD (no git needed); "" if it isn't a git checkout.

def project_features(data: dict) -> dict[str, list[str]]:
315def project_features(data: dict) -> dict[str, list[str]]:
316    """Features declared in a manifest's ``[features]`` table, each with the
317    (dotted) properties it sets, e.g. ``{"mpi": ["dependencies.mpi", "flags"]}``."""
318    table = data.get("features")
319    if not isinstance(table, dict):
320        return {}
321
322    def paths(value, prefix=""):
323        if isinstance(value, dict):
324            out = []
325            for key, sub in value.items():
326                out += paths(sub, f"{prefix}.{key}" if prefix else str(key))
327            return out
328        return [prefix]
329
330    return {str(name): paths(value) for name, value in table.items()}

Features declared in a manifest's [features] table, each with the (dotted) properties it sets, e.g. {"mpi": ["dependencies.mpi", "flags"]}.

def project_profiles(data: dict) -> dict[str, list[str]]:
333def project_profiles(data: dict) -> dict[str, list[str]]:
334    """Feature profiles declared in ``[profiles]`` (name -> list of features)."""
335    table = data.get("profiles")
336    if not isinstance(table, dict):
337        return {}
338    return {str(name): [str(f) for f in value] if isinstance(value, list) else []
339            for name, value in table.items()}

Feature profiles declared in [profiles] (name -> list of features).

def spec_key(declaration: Declaration) -> tuple:
351def spec_key(declaration: Declaration) -> tuple:
352    """What a declaration asks for, so that different ways of writing the
353    same thing compare equal."""
354    spec = declaration.spec
355    if isinstance(spec, str):
356        return ("version", spec.strip())
357    if not isinstance(spec, dict):
358        return ("other", repr(spec))
359    if "path" in spec:
360        return ("path", str(((declaration.parent_dir or Path()) / str(spec["path"])).resolve()))
361    if "git" in spec:
362        for key in ("tag", "branch", "rev"):
363            if key in spec:
364                value = str(spec[key]).strip()
365                # A commit may be written short or long: compare 7 characters.
366                return ("git", _git_url(str(spec["git"])), key, value[:7] if key == "rev" else value)
367        return ("git", _git_url(str(spec["git"])), "", "")
368    return ("registry", *sorted((str(k), str(v)) for k, v in spec.items()))

What a declaration asks for, so that different ways of writing the same thing compare equal.

def describe_spec(spec) -> str:
371def describe_spec(spec) -> str:
372    """A declaration in a few words: "tag v0.5.0", "path ../lib", "any version"…"""
373    if isinstance(spec, str):
374        return "any version" if spec.strip() == "*" else f"version {spec}"
375    if not isinstance(spec, dict):
376        return repr(spec)
377    if "path" in spec:
378        return f"path {spec['path']}"
379    if "git" in spec:
380        for key in ("tag", "branch", "rev"):
381            if key in spec:
382                value = str(spec[key])
383                return f"{key} {value[:7] if key == 'rev' else value} of {spec['git']}"
384        return f"default branch of {spec['git']}"
385    if "v" in spec:
386        return f"version {spec['v']}"
387    return ", ".join(f"{k} {v}" for k, v in spec.items())

A declaration in a few words: "tag v0.5.0", "path ../lib", "any version"…

def conflicts( graph: DependencyGraph) -> dict[str, list[Declaration]]:
390def conflicts(graph: DependencyGraph) -> dict[str, list[Declaration]]:
391    """Dependencies that different packages ask for differently (another
392    tag, commit, branch, repository or path). fpm uses only the declaration
393    closest to the project, silently: these are worth knowing about."""
394    out = {}
395    for name, declarations in graph.declarations.items():
396        if len({spec_key(d) for d in declarations}) > 1:
397            out[name] = declarations
398    return out

Dependencies that different packages ask for differently (another tag, commit, branch, repository or path). fpm uses only the declaration closest to the project, silently: these are worth knowing about.