Skip to content

bluehs: run scripts against the compiled library; make install-bluehs - #1119

Open
matx-jeffnewbern wants to merge 18 commits into
B-Lang-org:mainfrom
MatX-inc:jeffnewbern/bluehs
Open

matx-jeffnewbern wants to merge 18 commits into
B-Lang-org:mainfrom
MatX-inc:jeffnewbern/bluehs

Conversation

@matx-jeffnewbern

@matx-jeffnewbern matx-jeffnewbern commented Sep 14, 2026 •

Copy link
Copy Markdown
Collaborator

Stacks on #1117, which stacks on #1084; the diff includes their commits until they merge.

Adds bluehs, which runs Haskell scripts against the compiled bsc library, either from a checkout or from an installation.

In a checkout built with cabal (util/bluehs/setup.sh):

  • util/bluehs/bluehs <tool> [args] runs a program from src/comp/app with runghc instead of compiling it. runghc runs a file's main whatever its module is called, so the file an executable is built from is also its script, and there is no second copy.
  • util/bluehs/bin has one symlink per tool. The launcher dispatches on the name it was invoked as.
  • Any .hs file can be given by path. Each -i<dir> before it adds a directory to the import path, for a script split across modules.
  • cabal.project builds at -O2, like the make build, and writes a GHC environment file, so runghc and ghci find the library from anywhere in the tree. The launcher picks the environment file that matches its ghc.

The library has to be rebuilt whenever bsc is rebuilt at a new commit. .bo and .ba files carry the build version, and the library rejects a .ba from any other build.

make install-bluehs, run after make install-src, installs the same capability at inst/bluehs, so scripts can run on a machine with no native Haskell toolchain. Building it needs ghc, cabal, python3, and on Linux patchelf.

  • The installation holds a trimmed GHC, the compiled library and its dependencies, the solver libraries, the tool scripts, and the same launcher, which detects that it is running from an installation.
  • Library search paths are rewritten relative to each library (patchelf on Linux, install_name_tool on macOS), so inst can still be moved. The build fails if any path to the build machine remains.
  • Dependencies resolve against a fixed Hackage index-state.
  • GHC asks the C compiler where each library a package names is found. The system libraries that GHC's boot packages name already load with GHC, so the installation drops those names, and scripts that do not use CPP can run without a C compiler.
  • A smoke test runs a copy of the installation from another directory, in an empty environment, and checks that the library's build version matches inst/bin/bsc.

util/bluehs/mk-dist.sh builds the installation. make-ghc-pkg-info.sh now looks packages up by installed id, because cabal's store ids on macOS drop the vowels from package names.

Besides new files, the PR changes the top-level GNUmakefile, src/comp/make-ghc-pkg-info.sh, and five .gitignore lines.

Testing

  • DejaGNU with SystemC, GHC 9.10.3 on Debian: outcome-identical to the same compiler without these files (27,281 passes, 131 expected failures). The environment file was present throughout; the make build ignores it because it compiles with -hide-all-packages. The commits since that run change no compiler source.
  • make install-bluehs passes on Debian and on macOS with GHC 9.10.3, including the smoke test's run with no C compiler on PATH. On Debian, a two-module script and a script calling libc and libm through the FFI also run from a copy of the installation with no C compiler on PATH.
  • make-ghc-pkg-info.sh prints the same output as before for src/comp/Makefile's package list, on both platforms.
  • GHC 9.6.7 on macOS: a runghc script against a real .bo, and a ghci session calling into both solvers.

matx-amy and others added 18 commits August 24, 2026 14:36
This breaks the Makefile, and doesn't replace everything it did, but
it's sufficient to build binaries and run tests on Linux.
The bin wrapper scripts used to set BLUESPECDIR; with the solvers
statically linked, that was their last remaining job.  Instead, let
bsc, bluetcl, showrules and vcdcheck fall back to <exedir>/../lib
(recognized by its Libraries subdirectory) when BLUESPECDIR is unset,
and write the result back to the environment for the subprocesses
that read it (simulator build scripts, generated Bluesim executables,
and the Tcl side of bluetcl).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JW5cpDCo9whk4PCg9kn6ea
Every executable target runs `ghc --make` into the same -odir, so two of them
running at once race on the objects of the modules they share: one renames its
temporary over the path the other is still writing. Under `make -j10
install-extra` this appeared as

  renameFile:renamePath:rename '.../build/comp/VCD.o.tmp' to
    '.../build/comp/VCD.o': does not exist
  make: *** [fstcheck] Error 1

with two targets compiling VCD at the same time.

GNU Make 3.81 is still in use, and it has no per-target .NOTPARALLEL, so the
aggregate targets build through a sub-make invoked with -j1. That holds
whatever -j the caller passed. The cost is small because the objects are
shared: measured over a rebuild forced by touching VCD.hs, the whole serial
install-extra took seven seconds, since after the first target the rest
compile only their own main and link. GHCJOBS still parallelises within each
compile, and naming one executable directly still builds only that one.
The cabal build moves every program's entry module into src/comp/app, so that
they are executables separate from the library, and replaces
bluetcl_Main.hsc with a pre-generated bluetcl_Main.hs plus a C shim. The
Makefile did not follow, so it found no main at all: `ghc --make bsc` reports
`target 'bsc' is not a module name or a source file`.

Each moved target now names its own source file through MAINSRC, because the
file name does not always match the executable. Targets whose main is still
beside the library modules keep the default and are untouched. app joins the
module search path, so BlueTcl resolves as imported by bluetcl_Main.

bluetcl builds in one step from that entry module and shim, which is how the
cabal executable is built too, leaving one bluetcl entry point to maintain
rather than two. It gains the shared RTS options in the process, matching
what the cabal stanza gives the other executables.

-o $@ moves into the shared flags, because a target named by its app/ path
would otherwise link next to its source. The previous default output name was
already the target name, so nothing else changes, including the profiling and
debug builds.
Every bsc executable was installed as a copy of wrapper.sh in bin/. The
script set BLUESPECDIR from its own location, put $BLUESPECDIR/SAT on
LD_LIBRARY_PATH and DYLD_LIBRARY_PATH, and exec'd the real binary out of
bin/core/. getBluespecDir already derives the directory from the
executable's own path, so the library path was the only job left, and an
rpath does that without a script in the way.

On macOS an rpath is not enough on its own. Mach-O consults LC_RPATH only
for a dependency recorded as @rpath/..., and the vendored solvers record
bare names, which dyld resolves through its default search and nothing
else. Each solver therefore records itself @rpath-relative, through the
knob its own build already provides: -install_name for STP, and the
libyices_install_name that yices' configure accepts and that this tree was
already overriding to strip yices' absolute default. ELF needs none of it,
because a soname is what the linker writes into whatever links a library
and the installed files carry those names.

The rpath is @loader_path/../lib/SAT, or $ORIGIN/../lib/SAT on ELF, and it
is the only entry. It resolves bin/<name> to lib/SAT, where src/vendor
installs the solvers before src/comp installs anything. Being relative is
what keeps the top-level Makefile's promise that an installation can be
moved anywhere as long as its contents keep their relative positions, and
it leaves no build-tree path for a distribution to strip out of a shipped
binary. Only bsc and bluetcl link a solver, so only they carry it.

The executables now install directly as bin/<name> and bin/core is gone.
Nothing referred to that path.

The System 81 text loses its mention of BLUESPEC_LD_LIBRARY_PATH, which the
wrapper was the only reader of. LD_LIBRARY_PATH still overrides an rpath,
so the rest of that advice stands.
The version bounds are caret bounds set to the versions GHC 9.6.7 bundles, and
a caret bound on a compiler-bundled package admits exactly one GHC series.
base ^>=4.18.3.0 excludes 9.10's 4.20, and the same holds for bytestring,
containers, deepseq, filepath, text, unix and process. The make build has no
bounds at all and works from 9.6 through 9.14, so the cabal file is the only
thing tying the package to one compiler.

The thirteen bundled packages get plain lower bounds at 9.6.7's versions with
no upper bound, in every stanza: the library, custom-setup, the executables
and the test-suites. The Hackage dependencies -- old-locale, old-time,
regex-compat, split, strict-concurrency, syb -- keep their caret bounds, which
describe ranges rather than a compiler.

process needed its floor moved down rather than up. ^>=1.6.28.0 excludes the
version both compilers bundle (1.6.19.0 on 9.6.7, 1.6.26.1 on 9.10.3), so
cabal built process from Hackage on every machine, and Hackage's 1.6.30.0 does
not compile against the filepath GHC 9.10 ships.

That floor was carrying something: SetupHooks.hs used callCreateProcess, which
System.Process first exported in process-1.6.28. It is four lines over
createProcess and waitForProcess, so it is spelled out locally instead, and
the setup hooks build against whatever process the compiler brings.

Verified by a full cabal build, library and every executable, under GHC 9.6.7
and under 9.10.3. 9.12 and 9.14 are untested: the bounds no longer exclude
them, but nothing has been compiled against them.
The solvers arrived as static archives carried in extra-bundled-libraries,
which serves the executables and defeats everything that loads the library
rather than linking it. Cabal keeps bundled libraries off the bundling
component's own link line, so the dynamic object that ghci and runghc load
had 186 unresolved solver symbols and failed at dlopen. Declaring the same
archives in extra-libraries instead puts them on that link line, but then
GHC's runtime linker tries to load them and cannot: a static archive on
aarch64-darwin hits an unsupported relocation type and aborts.

So the solvers are shared libraries again, as they were before this package
existed. The configure hook builds the vendored trees and injects their
directories into every component through extraLibs and extraLibDirs, the way
the Tcl hook injects what platform.sh reports, because an absolute path in a
build tree cannot be written in the .cabal file.

It also names an rpath, which is the whole of what either platform needs to
resolve the solvers now that they record themselves as @rpath/..., and the
vendored directories are the right entry because these artifacts run where
they are built. Naming it on the library alone is enough: cabal derives none
of its own for these components, and an executable that links the library
inherits the library's ldOptions. Injecting it per component instead passes
each -rpath twice, which the linker warns about on every link.

util/bluehs/setup.sh loses the second build and the generated
cabal.project.local it used to write, which existed only to link the archives
into the dynamic object after the fact. The launcher loses
-Wno-missed-extra-shared-lib: GHC emitted that warning because the
registration named libraries with no shared flavour to find, and now there is
one.

Verified under GHC 9.6.7 on macOS and GHC 9.10.3 on Linux: the library and
all eight executables record both solvers and run, a util/bluehs script and a
ghci session load the library and call into both solvers, and the warning is
gone.
The HLS and GHCi checks run from src/comp and name bsc.hs, which now lives in
src/comp/app. HLS falls back to reading the argument as a directory and fails
with "Couldn't find any .hs/.lhs files inside directory: bsc.hs", on every
platform and GHC. The GHCi check names the same missing file behind it.

gen_hie.py builds the HLS cradle from a list of directories that might hold
Haskell sources, and .ghci sets the GHCi include path from its own list.
Neither had app, so the entry modules were in no include path and in no source
list. Both gain it, and the two checks name app/bsc.hs.

Verified on macOS under GHC 9.10.3: gen_hie.py writes a cradle listing all nine
app sources and -i./app; `haskell-language-server-9.10.3 app/bsc.hs` completes
with 1 file worked, 0 failed; and `:load app/bsc.hs` loads 227 modules, which
the workflow's own grep reads as a pass.

Co-authored-by: Claude <ai.claude@matx.com>
…rary

With the compiler built as a cabal library and the program entry modules under
src/comp/app, those programs can also be run by runghc against the compiled
library rather than as binaries of their own, and ghci can import the whole
compiler.

There is no second copy of any program. runghc executes a file's main whatever
the module is called, so src/comp/app/dumpbo.hs -- module Main_dumpbo, the
same file the dumpbo executable is built from -- is itself the script. The
launcher resolves a tool name to that file, and bin/ holds one symlink per
tool, dispatching on the name it was invoked as.

cabal.project names the repository as the project, asks for -O2 to match the
make build, and sets write-ghc-environment-files: always, so a bare runghc or
ghci resolves the compiled library from anywhere in the tree. The launcher
picks the environment file matching the version of the ghc it will run, so a
tree built for one compiler is never handed to another.

setup.sh is a single cabal build. It exists for its documentation as much as
the command: the library has to be rebuilt whenever bsc is rebuilt at a new
commit, because .bo and .ba files embed the build version string and the
library refuses a .ba whose stamp differs from its own.
Builds bsc-bluehs-<os>-<arch>-<version>.tar.gz, the companion artifact
to the main bsc tarball: a relocatable tree with a pruned GHC runtime
(no profiling/static ways, docs, or unused tools), a ${pkgroot}-relative
package store holding the bsc library and its dependencies, the SAT
solver shared libraries, the utility scripts, and a bin/bluehs launcher.
Tarball users run Haskell scripts against the compiler library with no
Haskell toolchain installed; host requirements are glibc, libgmp,
libtcl8.6 and a C compiler (GHC probes it when loading libraries, and
CPP scripts preprocess with it).

Both tarballs must come from the same commit: the packaged library
embeds the build version string and rejects .ba files from a different
bsc.  Ship them from one release action, versioned in lockstep.

LICENSES/ covers everything redistributed: LICENSE.ghc, the SAT solver
texts, and a generated LICENSE.ghc_pkgs enumerating every shipped
Haskell package with license and copyright (make-ghc-pkg-info.sh over
the exact shipped closure, which also fails the build if a non-BSD
license appears).

The build smoke-tests the assembled tree in a scrubbed environment
before packaging.  Validated end-to-end on linux-x86_64/GHC 9.6.7
(106 MB compressed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The library is built in place rather than installed: installing a local
package goes through a source distribution, which lacks the vendored
solver sources the hooks build. The in-place library then joins the
shipped store as one more package. The tool entry scripts come from
src/comp/app; the launcher puts a script's directory on the import path.
`bluehs [-i<dir>]... <tool|script.hs> [args...]` passes each -i to runghc,
so a script split across several modules can run. The arguments stay in
"$@", so a directory containing spaces survives.

The same launcher also runs from an installed distribution: when
<dir>/../hs/bsc.env exists beside it, it uses the bundled runghc, that
package environment and the scripts directory, and defaults BLUESPECDIR to
the lib directory of the bsc installation around it.
`make install-bluehs`, after `make install-src`, runs mk-dist.sh to install
the distribution at inst/bluehs instead of writing a separate tarball, so it
ships inside the bsc installation it must match. It installs the bluehs
launcher rather than generating its own copy.

- Every search path and dependency of the packaged shared libraries that
  names the build machine is rewritten relative to the library, with
  patchelf on Linux and install_name_tool on macOS (re-signed ad hoc), or
  dropped; the script fails if one survives. The solvers then load without
  LD_LIBRARY_PATH.
- The generated cabal.project pins an index-state, so the commit fixes the
  dependency set.
- cabal's store directory is found rather than assumed, since newer cabal
  appends an ABI hash to its name.
- The smoke test runs a copy of the installed tree from another directory,
  with the build tree moved aside, under an empty environment. It calls
  both solvers and checks that the library's build version matches the
  installation's bsc.
GHC's loader asks the C compiler (--print-file-name) for every library a
package names, and GHC's boot packages name system libraries: libc, libm,
libgmp, librt, libdl, libstdc++ and libtinfo. Those already load as
dependencies of GHC and of the packages' shared objects, so the names are
dropped from the distribution's global package database, as the bsc
library's registration already drops Tcl and zlib. A script then runs where
no C compiler is on PATH, such as a Bazel sandbox that hides the host's;
only scripts that use CPP still need one.

The smoke test runs its probe with nothing on PATH but what the launcher and
the GHC wrappers use.
The script recovered a package's name by stripping the hash from its
installed id, then looked it up with ghc-pkg field <name-version>. On
macOS cabal-install drops the vowels from the name when it forms a
store id (old-locale becomes ld-lcl-1.0.0.7-1ff3ddac), so walking a
cabal store failed with "cannot find package ld-lcl-1.0.0.7" and
stopped mk-dist.sh at its LICENSES step.

Resolve each command-line name to its id once, look every field up
with --ipid, and take the package name from the name field. Output for
the release tarball's package list is unchanged.
@matx-jeffnewbern matx-jeffnewbern changed the title bluehs: run the app entry modules as scripts against the compiled library bluehs: run scripts against the compiled library; make install-bluehs Sep 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants