%global srcname judge # judge's runner opens with (import cmd) and its project.janet pins the tag, so # there is no judge that works without it. cmd is packaged nowhere else. %global cmdname cmd %global cmdver 1.1.0 # cmd installs as a private copy below judge's own module directory rather than # at %%{_libdir}/janet/%%{cmdname}. Owning that shared name would collide with a # future janet-cmd and would quietly make this package the system-wide provider # of a library it only vendors, which the bundled() tag below would then misstate. # # The copy is addressed by module name, not by a search path, so the directory # is written once and the name derived from it. %global vendormod %{srcname}/_vendor %global vendordir %{_libdir}/janet/%{vendormod} # Pure-Janet module package with no compiled output, so no debuginfo. It still # is not noarch: janet's syspath is %%{_libdir}/janet, and the binscript bakes # that path in, so both the payload and the script are arch-dependent. %global debug_package %{nil} Name: janet-%{srcname} Version: 2.11.0 Release: 1%{?dist} Summary: Self-modifying inline snapshot test framework for Janet License: MIT URL: https://github.com/ianthehenry/%{srcname} Source0: %{url}/archive/v%{version}/%{srcname}-%{version}.tar.gz Source1: https://github.com/ianthehenry/%{cmdname}/archive/v%{cmdver}/%{cmdname}-%{cmdver}.tar.gz # Only to run %%check. Both upstreams are pure Janet, which is why neither gcc # nor janet-devel appears here. BuildRequires: janet >= 1.41 # Pure Janet source records no janet.h, so there is no ABI ceiling to derive the # way a native module needs one. The floor is upstream's: judge 2.11.0's # changelog requires Janet 1.41 for the module loader path improvements, which is # newer than the 1.35.2 stock Fedora carries. Requires: janet%{?_isa} >= 1.41 Provides: bundled(janet-%{cmdname}) = %{cmdver} %description Judge is a test framework for Janet built on inline snapshots. Instead of writing assertions, you write expressions to observe, and the test runner fills in the expected value for you: (test (+ 1 2)) becomes (test (+ 1 2) 3) A run that disagrees with a recorded expectation writes a .tested file beside the source rather than editing it, leaving the merge to you. Outside the runner every macro Judge exposes is a no-op, so a library's tests never execute during normal evaluation. %prep %autosetup -n %{srcname}-%{version} -a 1 # cmd's LICENSE is byte-identical to judge's, down to the copyright holder and # year, so %%license ships one copy for both rather than two of the same text. # A version bump that makes them diverge would leave the vendored code's terms # unstated, so it stops the build. if ! cmp %{cmdname}-%{cmdver}/LICENSE LICENSE; then echo 'cmd LICENSE diverged: ship it as a second %%license file' >&2 exit 1 fi %build # The payload path below and the paths the script names are both janet's module # path, written out. A janet that moved it must red the build rather than # ship a package its own loader cannot find. janet -e '(def p (dyn :syspath)) (assert (= p "%{_libdir}/janet") (string "janet syspath moved to " p))' # Upstream's script is generated by janet-pm's declare-binscript with # :hardcode-syspath true, which bakes the path it was installed under into the # script. Under rpmbuild that path is a buildroot the target does not have. # Nothing here runs janet-pm at all, the modules being plain file copies, so # there is no generated script to rewrite either. # # Judge's own modules are appended to module/paths rather than put through # root-env :syspath. JANET_PATH replaces the syspath, so overriding it costs # the runner every module under test: judge would load and a project's own # dependencies beside it would not. Appending resolves judge with no JANET_PATH # and with one, and leaves a project free to prefer its own copy. # # cmd is not on that list. A search-path entry for it would be answered by any # cmd the invocation can already see, which for a janet-pm project that depends # on judge is the common case, so the vendored copy is imported by name below # and in runner.janet instead. cat > %{srcname}.janet <<'EOF' #!/usr/bin/janet (array/push module/paths ["%{_libdir}/janet/:all:.janet" :source] ["%{_libdir}/janet/:all:/init.janet" :source]) (import %{vendormod}/%{cmdname}) (import %{srcname}/runner) (cmd/main runner/main) EOF %install # Both upstreams enumerate their modules in declare-source, and in both trees # that set is exactly src/*.janet. judge's binscript also lives in src/ but # carries no extension, so the glob leaves it behind. install -dm0755 %{buildroot}%{_libdir}/janet/%{srcname} %{buildroot}%{vendordir}/%{cmdname} install -pm0644 src/*.janet %{buildroot}%{_libdir}/janet/%{srcname}/ install -pm0644 %{cmdname}-%{cmdver}/src/*.janet %{buildroot}%{vendordir}/%{cmdname}/ install -Dpm0755 %{srcname}.janet %{buildroot}%{_bindir}/%{srcname} # runner.janet opens with a bare (import cmd), which whatever cmd the syspath # offers would answer. Name the private copy instead. A module's last segment # is the prefix it binds, so every cmd/ call site in that file keeps working # untouched. The grep covers the whole installed tree, not just the file the # sed names, so a version that moved or added the import reds here rather than # shipping a module this package does not resolve. sed -i 's|^(import %{cmdname})$|(import %{vendormod}/%{cmdname})|' \ %{buildroot}%{_libdir}/janet/%{srcname}/runner.janet if grep -rF '(import %{cmdname})' %{buildroot}%{_libdir}/janet/%{srcname}/; then echo 'a bare cmd import survived the rewrite' >&2 exit 1 fi %check # Upstream tests judge and cmd with cram against the exact stdout of an older # Janet. Under 1.41+ every suite reds on output that has since changed: new # "binding shadows a macro" warnings, and a different order for cmd's argument # errors. The suites are upstream's development harness, so this exercises the # staged tree directly instead. # Everything below has to resolve against the staged tree and nothing else. # The paths the script appends sit behind the syspath by design, so on a host # that already carries janet-judge an unpinned run would exercise the installed # copy and pass without ever reading what was just built. export JANET_PATH=%{buildroot}%{_libdir}/janet # The staged script names the final module paths, so retarget a copy at the # buildroot. This runs the generated script itself rather than a stand-in, and # the grep is what keeps that true: a sed that quietly stopped matching would # leave a byte copy naming paths outside the buildroot. sed 's|%{_libdir}/janet|%{buildroot}%{_libdir}/janet|g' \ %{buildroot}%{_bindir}/%{srcname} > check-%{srcname} grep -q '%{buildroot}' check-%{srcname} chmod +x check-%{srcname} # --help drives the whole import chain: the vendored cmd, then runner, which # pulls in rewriter and colorize. With the judge import below, that compiles # every module the package ships. ./check-%{srcname} --help > /dev/null # Both claims below are about the staged tree, so the first one is that it is # the tree being read: without it an export that stopped reaching here would # leave the import testing the build host and the absence of cmd trivially # true. The second is that the shared name stays unclaimed, or the bundled() # tag misstates what this package provides and a future janet-cmd would fight # it for the same directory. janet -e '(def [staged _] (module/find "%{srcname}")) (assert (string/has-prefix? "%{buildroot}" (or staged "")) (string "judge resolved outside the buildroot: " staged)) (import %{srcname}) (def [leaked _] (module/find "%{cmdname}")) (assert (nil? leaked) "cmd leaked onto the syspath")' # Never -a in the runs below: that rewrites sources in place. mkdir -p smoke cd smoke printf '(use %{srcname})\n(test (+ 1 2))\n' > unfilled.janet printf '(use %{srcname})\n(test (+ 1 2) 3)\n' > filled.janet if ../check-%{srcname} unfilled.janet > /dev/null; then echo 'an unfilled (test) must exit nonzero' >&2 exit 1 fi grep -qxF '(test (+ 1 2) 3)' unfilled.janet.tested ../check-%{srcname} filled.janet > /dev/null if [ -e filled.janet.tested ]; then echo 'a filled (test) must not write a .tested file' >&2 exit 1 fi # A project's own JANET_PATH must keep owning resolution: the runner has to see # the module under test, not only itself. This reds if the script ever goes # back to putting a syspath. mkdir -p dep/mylib printf '(defn greet [] "hi")\n' > dep/mylib/init.janet printf '(use %{srcname})\n(import mylib)\n(test (mylib/greet) "hi")\n' > project.janet JANET_PATH=$PWD/dep ../check-%{srcname} project.janet > /dev/null cd .. %files %license LICENSE %doc README.md %{_bindir}/%{srcname} %{_libdir}/janet/%{srcname}/ %changelog * Sun Sep 27 2026 Bahtiar `kalkin-` Gadimov - 2.11.0-1 - Initial package - Bundle cmd as a private copy, keeping %%{_libdir}/janet/cmd unclaimed - Write the binscript in the spec, upstream's hardcodes a buildroot syspath