From 993415712e3bc7381ea2253e0e1d37f15c14bc8a Mon Sep 17 00:00:00 2001 From: sammuli Date: Wed, 2 Sep 2026 10:52:42 -0700 Subject: [PATCH 1/2] feat: make the CLI reachable as `python -m fdp` The `fdp` console script is not always reachable. Graphviz installs its force-directed layout engine at the same path -- bin/fdp, one of the eight engines it ships -- and conda has no conflict detection: conda-meta records graphviz as the owner and graphviz wins. Any environment containing cmflib pulls graphviz transitively (cmflib -> dvc -> pydot -> graphviz), so `fdp run` there silently invokes a graph layout tool. `python -m fdp` resolves through the installed package rather than PATH, so it works regardless. Additive: the console script is untouched. Sets sys.argv[0] so usage and error messages say "python -m fdp" rather than "__main__.py", which is what argparse would otherwise derive. A rename of the console script is planned for a major release; see docs/2026-09-02-fdp-cli-rename.md in the workspace. This is useful during that transition and afterwards. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Kpkx3jFwQxuBRqoVcw6f27 --- fdp/__main__.py | 40 +++++++++++++++++++++ tests/test_module_entry_point.py | 60 ++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 fdp/__main__.py create mode 100644 tests/test_module_entry_point.py diff --git a/fdp/__main__.py b/fdp/__main__.py new file mode 100644 index 0000000..8883f57 --- /dev/null +++ b/fdp/__main__.py @@ -0,0 +1,40 @@ +# Copyright 2026 General Atomics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Make the FDP CLI reachable as ``python -m fdp``. + +The ``fdp`` console script is not always reachable. Graphviz installs its +force-directed layout engine at the same path -- ``bin/fdp``, one of the eight +engines it ships -- and conda has no conflict detection for that: ``conda-meta`` +records graphviz as the owner and graphviz wins. Any environment containing +``cmflib`` pulls graphviz transitively (cmflib -> dvc -> pydot -> graphviz), so +in those environments ``fdp run`` silently invokes a graph layout tool. + +``python -m fdp`` resolves through the installed package rather than ``PATH``, +so it works regardless. Prefer it in scripts and documentation that must run in +environments carrying the provenance stack. + +See ``docs/2026-09-02-fdp-cli-rename.md`` in the workspace for the full +analysis; a rename of the console script is planned for a major release. +""" + +import sys + +from fdp.cli import main + +if __name__ == "__main__": + # argparse derives prog from basename(sys.argv[0]), which is + # "__main__.py" under -m. Usage and error messages should show something + # the reader can actually type. + sys.argv[0] = "python -m fdp" + main() diff --git a/tests/test_module_entry_point.py b/tests/test_module_entry_point.py new file mode 100644 index 0000000..ced4e3d --- /dev/null +++ b/tests/test_module_entry_point.py @@ -0,0 +1,60 @@ +# Copyright 2026 General Atomics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""`python -m fdp` must stay reachable. + +The `fdp` console script is not always reachable: graphviz installs its +force-directed layout engine at the same `bin/fdp` path, conda records graphviz +as the owner, and graphviz wins. Any environment with cmflib pulls graphviz +transitively, so `fdp run` there invokes a layout tool. `python -m fdp` +resolves through the package rather than PATH. +""" + +import subprocess +import sys +import unittest + + +class TestModuleEntryPoint(unittest.TestCase): + def _run(self, *args): + return subprocess.run( + [sys.executable, "-m", "fdp", *args], + capture_output=True, text=True, timeout=60, + ) + + def test_module_invocation_works(self): + self.assertEqual(self._run("--help").returncode, 0) + + def test_every_subcommand_is_reachable(self): + out = self._run("--help").stdout + for command in ("run", "env", "login", "logout", "ls", "catalog", + "device", "skills"): + self.assertIn(command, out) + + def test_usage_shows_something_typeable(self): + # argparse defaults prog to basename(sys.argv[0]), which is + # "__main__.py" under -m -- useless in an error message. + out = self._run("--help").stdout + self.assertIn("python -m fdp", out) + self.assertNotIn("__main__.py", out) + + def test_errors_name_the_module_form(self): + err = self._run("nosuchcommand").stderr + self.assertIn("python -m fdp", err) + + def test_it_is_the_same_cli(self): + from fdp.cli import main + + import fdp.__main__ as entry + + self.assertIs(entry.main, main) From 31fefa6bdc91357a6ce7ca0fb73abd510d5702c4 Mon Sep 17 00:00:00 2001 From: sammuli Date: Wed, 2 Sep 2026 20:43:33 -0700 Subject: [PATCH 2/2] fix: take bin/fdp back from graphviz by depending on it graphviz ships `bin/fdp` -- a symlink to `dot` that dispatches on argv[0] -- and so do we. Anything pulling graphviz into an FDP environment lands a second owner on `$PREFIX/bin/fdp`, and the CMF provenance stack does exactly that: cmflib -> dvc -> pydot -> graphviz. Measured on pixi, on a clean env with `fdp` and `cmflib`: bin/fdp -> dot $ fdp -V fdp - graphviz version 14.1.2 The FDP CLI is not shadowed, it is *gone*: `fdp run python ...` becomes a graph layout tool. There is nothing to warn the user with, because our entry point is the file that got replaced -- which is why a deprecation notice could never have worked here. This is what has blocked putting cmflib and toksearch_cmf into the fdp-core metapackage. Declaring graphviz as our own run dependency fixes it: the installer's topological link order puts a dependency before its dependent, so graphviz links first, `fdp` links last, and we take the file. Whether that is a fix or an insurance policy depends on the installer, and both are worth having: pixi/rattler -- graphviz genuinely wins without this; the CLI is broken today. With it we win from scratch (3/3 runs, deterministic), on an incremental `pixi add cmflib`, and across a real graphviz 13.1.2 -> 14.1.2 upgrade. That last case means rattler respects existing file ownership rather than merely link order. This is the path `fdp-install` uses, so it is where most users live. micromamba 2.9.0 -- already linked `fdp` last without this dependency (graphviz at step 1193, fdp at 1266) and so already worked. Nothing enforced that order though; it fell out of the solve. This makes it guaranteed instead of lucky. Nothing is lost on the graphviz side: `dot -Kfdp` is the documented equivalent of its `fdp` binary. This relies on installer behaviour that is not a documented guarantee, so it ships with a recipe test rather than a comment alone. graphviz is now a run dependency, so the test environment reproduces the collision for free; the test fails loudly, naming the cause, if graphviz ever wins again. If it does, the answer is to rename the CLI -- but until then no rename, no deprecation, and no user-visible change. Also asserts `python -m fdp` keeps working: it is the one entry point no other package can take, and the fallback if this ever regresses. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Kpkx3jFwQxuBRqoVcw6f27 --- recipe/recipe.yaml | 62 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/recipe/recipe.yaml b/recipe/recipe.yaml index 07a7534..574e087 100644 --- a/recipe/recipe.yaml +++ b/recipe/recipe.yaml @@ -36,6 +36,41 @@ requirements: - pelicanplatform >=7.24.3,<8 - fdp-schema >=0.2.1 - toksearch + # DELIBERATE AND LOAD-BEARING -- do not remove as an unused dependency. + # + # graphviz ships `bin/fdp` (a symlink to `dot`, dispatching on argv[0]) + # and so do we. Anything that pulls graphviz into an FDP environment -- + # cmflib -> dvc -> pydot -> graphviz, which is the whole CMF provenance + # stack -- lands a second owner on `$PREFIX/bin/fdp`. Whichever package + # is linked last wins the file, and without this dependency graphviz can + # win: `fdp run python ...` then silently becomes a graph layout tool and + # the FDP CLI is simply gone. There is no warning to print, because our + # entry point is the thing that got replaced. + # + # Declaring graphviz here makes it our *dependency*, so the installer's + # topological link order puts graphviz first and `fdp` last, and we take + # the file. + # + # Which installer you use decides whether this is a fix or an insurance + # policy, and both are worth having: + # + # pixi/rattler -- graphviz genuinely wins without this dependency; the + # CLI is broken today. With it we win from scratch (3/3 runs), on an + # incremental `pixi add cmflib`, and across a real graphviz + # 13.1.2 -> 14.1.2 upgrade. That last case means rattler respects + # existing file ownership, not merely link order. + # + # micromamba 2.9.0 -- happens to link `fdp` last even without this + # dependency (graphviz at step 1193, fdp at 1266), so it already + # worked. But nothing *enforced* that order; it was incidental to the + # solve. This dependency makes it guaranteed rather than lucky. + # + # Nothing is lost on the graphviz side: `dot -Kfdp` is the documented + # equivalent of its `fdp` binary. + # + # The test below is the guard. If it ever fails, this trick has stopped + # working and the CLI needs renaming instead. + - graphviz tests: - script: @@ -45,6 +80,33 @@ tests: source: - tests/ + # Guard for the graphviz `bin/fdp` collision explained in the run + # requirements. graphviz is a run dependency, so this test environment + # reproduces the collision for free -- if `fdp` here is graphviz's binary + # rather than ours, the CLI is broken for every user with cmflib installed. + - script: + content: + - | + set -e + echo "bin/fdp resolves to: $(readlink -f "$(command -v fdp)")" + # graphviz's fdp answers -V with "fdp - graphviz version ...". + if fdp -V 2>&1 | grep -qi "graphviz"; then + echo "ERROR: bin/fdp is graphviz's binary, not the FDP CLI." + echo "The link-order trick (graphviz as a run dep) has stopped" + echo "working. The CLI must be renamed instead. See the comment" + echo "beside the graphviz run dependency in recipe.yaml." + exit 1 + fi + # Ours must actually work, not merely be present. + fdp --help | grep -qE "\brun\b" || { + echo "ERROR: 'fdp --help' does not look like the FDP CLI:"; fdp --help | head -5; exit 1; } + echo "OK: bin/fdp is the FDP CLI." + # The unclobberable fallback: even if bin/fdp is ever lost, this + # entry point cannot be taken by another package. + - | + python -m fdp --help > /dev/null + echo "OK: python -m fdp works" + about: homepage: https://github.com/GA-FDP/fdp license: Apache-2.0