From fe475e774afd17be15be85cbbdd79162e553ba52 Mon Sep 17 00:00:00 2001 From: Andy Kopra Date: Fri, 25 Sep 2026 00:32:55 +0200 Subject: [PATCH] Klammertext runs natively on Windows; a Windows source-install guide; the bundled sh for @eval :shell Assembled from klammertext-dev f344a37d0f6e. The commands, the library, the Standard Klammer Set, the reflection klammerset and the Python binding build and run natively on Windows (MSYS2 UCRT64 gcc, python.org Python), and both test tiers pass there. doc/install/windows_source_install.md is the fifth install guide: a source install parallel to the Linux and macOS ones, verified by following it on a pristine Windows 11. Behaviour on every system: paths Klammertext prints use forward slashes; output is LF; the command line is UTF-8 on Windows; @eval :shell is a POSIX shell command everywhere -- on Windows it runs the sh Klammertext ships, BusyBox for Windows (win/busybox/, GPLv2, with its licence and the source pointer). KLAMMERTEXT_FONTS and KLAMMERTEXT_KLAMMERSETS are lists in the system's PATH convention (';' on Windows). On Linux and macOS nothing observable changes. --- README.md | 2 +- com/Makefile | 37 ++- com/kdesc.cpp | 2 +- com/kdiag.cpp | 4 +- com/klammertext.manifest | 14 ++ com/klammertext.rc | 4 + doc/install/windows_source_install.md | 305 ++++++++++++++++++++++ env/makefile.env | 38 ++- env/runtime.env | 26 +- kpy/Makefile | 24 +- kpy/klammertext.cpp | 6 +- mac/argv.h | 2 +- mac/command.cpp | 25 +- mac/command.h | 7 + mac/coverage.cpp | 6 +- mac/error.cpp | 2 +- mac/eval.cpp | 16 +- mac/file.cpp | 77 +++++- mac/file.h | 22 ++ mac/font_store.cpp | 52 ++-- mac/katomize.cpp | 5 + mac/katomize.h | 4 + mac/klammerset_registry.cpp | 26 +- mac/locator.cpp | 12 +- mac/locator.h | 2 +- mac/machine.cpp | 16 +- mac/state.cpp | 4 +- mac/util.cpp | 47 +++- mac/util.h | 22 ++ ref/Makefile | 2 +- sks/document/Makefile | 2 +- sks/document/document_class.cpp | 4 +- sks/document/document_html.cpp | 6 +- sks/document/document_latex.cpp | 2 +- sks/document/heading.cpp | 4 +- sks/image/image.py | 4 +- sks/kutil/kutil.cpp | 4 +- sks/kutil/kutil.py | 4 +- sks/target/latex_util.cpp | 7 +- tst/command_option_test.sh | 2 +- tst/cond_test.sh | 4 +- tst/coverage_test.sh | 9 +- tst/editor_test.sh | 15 +- tst/escape_test.sh | 4 +- tst/eval_test.sh | 13 +- tst/filename_test.sh | 9 + tst/kdesc_test.sh | 15 +- tst/kdiag_test.sh | 2 +- tst/klammerset_test.sh | 6 +- tst/kpy_test.sh | 19 +- tst/modulepath_test.sh | 7 + tst/verbosity_test.sh | 6 +- win/busybox/LICENSE | 348 ++++++++++++++++++++++++++ win/busybox/README.md | 28 +++ win/busybox/busybox64u.exe | Bin 0 -> 675840 bytes 55 files changed, 1182 insertions(+), 153 deletions(-) create mode 100644 com/klammertext.manifest create mode 100644 com/klammertext.rc create mode 100644 doc/install/windows_source_install.md create mode 100644 win/busybox/LICENSE create mode 100644 win/busybox/README.md create mode 100644 win/busybox/busybox64u.exe diff --git a/README.md b/README.md index fdd6817..4880a59 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ are regenerated on each release — patches cannot be merged directly. Report problems (or send patches) to the author; accepted changes are applied to the development tree and appear in a following snapshot. -This snapshot was assembled from development commit `daad6273483a`. +This snapshot was assembled from development commit `f344a37d772c`. ## License diff --git a/com/Makefile b/com/Makefile index 5680833..7c6cf42 100644 --- a/com/Makefile +++ b/com/Makefile @@ -28,6 +28,20 @@ LIBRARY := $(LIBDIR)/libklammertext.so # Linker flags for commands COM_LDFLAGS := $(EXPORT_DYNAMIC) -Wl,-rpath,'$(ORIGIN)/../lib' -L$(LIBDIR) +# Windows: the application manifest (UTF-8 process code page) is linked into +# each command as a resource object; elsewhere there is nothing to add. +# (Only variables here: a rule before "all" would become the default goal. +# The Windows rules themselves follow the commands target below.) +ifneq (,$(filter MINGW% MSYS%,$(UNAME_S))) +MANIFEST_OBJ := manifest.o +# The DLLs beside the commands (makefile.env explains); with them, bin/ on +# PATH is all a Windows user needs besides Python. +RUNTIME_DLLS := $(addprefix $(BINDIR)/,libklammertext.so $(GCC_RUNTIME_DLLS)) +else +MANIFEST_OBJ := +RUNTIME_DLLS := +endif + # Dependency files for command sources DEPFILES := $(addsuffix .d,$(COMMANDS)) @@ -46,7 +60,16 @@ all : | mac sks # Separate target to build commands (called after dependencies are ready) .PHONY: commands -commands : $(BINCOMMANDS) +commands : $(BINCOMMANDS) $(RUNTIME_DLLS) + +ifneq (,$(filter MINGW% MSYS%,$(UNAME_S))) +manifest.o : klammertext.rc klammertext.manifest + windres $< $@ +$(BINDIR)/libklammertext.so : $(LIBRARY) | $(BINDIR) + cp $< $@ +$(BINDIR)/%.dll : $(GCC_BIN)%.dll | $(BINDIR) + cp $< $@ +endif # Ensure the output directory exists before linking into it. $(BINDIR) : @@ -63,14 +86,14 @@ sks : | mac $(MAKE) -C $(KS)/document # Explicit rules for each command - link against shared library, output to bin/ -$(BINDIR)/kdiag : kdiag.cpp $(LIBRARY) | $(BINDIR) - $(CXX) $(CPPFLAGS) $(CXXFLAGS) $(DEPFLAGS) $(COM_LDFLAGS) $(LDFLAGS) $< -o $@ -lklammertext $(LDLIBS) +$(BINDIR)/kdiag : kdiag.cpp $(LIBRARY) $(MANIFEST_OBJ) | $(BINDIR) + $(CXX) $(CPPFLAGS) $(CXXFLAGS) $(DEPFLAGS) $(COM_LDFLAGS) $(LDFLAGS) $< $(MANIFEST_OBJ) -o $@ $(KLAMMERTEXT_LIB) $(LDLIBS) -$(BINDIR)/kdesc : kdesc.cpp $(LIBRARY) | $(BINDIR) - $(CXX) $(CPPFLAGS) $(CXXFLAGS) $(DEPFLAGS) $(COM_LDFLAGS) $(LDFLAGS) $< -o $@ -lklammertext $(LDLIBS) +$(BINDIR)/kdesc : kdesc.cpp $(LIBRARY) $(MANIFEST_OBJ) | $(BINDIR) + $(CXX) $(CPPFLAGS) $(CXXFLAGS) $(DEPFLAGS) $(COM_LDFLAGS) $(LDFLAGS) $< $(MANIFEST_OBJ) -o $@ $(KLAMMERTEXT_LIB) $(LDLIBS) -$(BINDIR)/ktext : ktext.cpp $(LIBRARY) | $(BINDIR) - $(CXX) $(CPPFLAGS) $(CXXFLAGS) $(DEPFLAGS) $(COM_LDFLAGS) $(LDFLAGS) $< -o $@ -lklammertext $(LDLIBS) +$(BINDIR)/ktext : ktext.cpp $(LIBRARY) $(MANIFEST_OBJ) | $(BINDIR) + $(CXX) $(CPPFLAGS) $(CXXFLAGS) $(DEPFLAGS) $(COM_LDFLAGS) $(LDFLAGS) $< $(MANIFEST_OBJ) -o $@ $(KLAMMERTEXT_LIB) $(LDLIBS) # Out-of-tree system install (copies; the build tree stays intact). The # installed commands still need KLAMMERTEXT_HOME pointing at a Klammertext tree diff --git a/com/kdesc.cpp b/com/kdesc.cpp index 4286498..bba00f6 100644 --- a/com/kdesc.cpp +++ b/com/kdesc.cpp @@ -309,7 +309,7 @@ int main(int argc, char* argv[]) M.m_state.set( "K_input_dir", absolute_pathname(file_directory(input_filenames[0]))); } else { - M.m_state.set("K_input_dir", fs::current_path().string()); + M.m_state.set("K_input_dir", fs::current_path().generic_string()); } // --klammersets both lists and loads: with no words it enumerates the diff --git a/com/kdiag.cpp b/com/kdiag.cpp index 4a1d8e4..a1d81fc 100644 --- a/com/kdiag.cpp +++ b/com/kdiag.cpp @@ -74,7 +74,7 @@ int main(int argc, char* argv[]) } if (p("rewrite")) { - show_rewrite_rules = true; + set_show_rewrite_rules(true); } // kdiag deliberately has no --klammersets: a klammerset is reached here @@ -94,7 +94,7 @@ int main(int argc, char* argv[]) machine.m_state.set("K_target", Target_registry::general_name); machine.m_state.set("K_input_dir", fs::current_path().string()); std::string input = args.as_string("input"); - std::string command = construct_command_pathname(argv[0]); + std::string command = construct_command_pathname(argv[0]).generic_string(); // ONE authoritative katom list. process() returns the katoms but // registers nothing -- read() is what appends to m_katoms and extracts // the definitions -- so kdiag does that itself, in the order read() diff --git a/com/klammertext.manifest b/com/klammertext.manifest new file mode 100644 index 0000000..d8e39fc --- /dev/null +++ b/com/klammertext.manifest @@ -0,0 +1,14 @@ + + + + + + + UTF-8 + + + diff --git a/com/klammertext.rc b/com/klammertext.rc new file mode 100644 index 0000000..5c89f7e --- /dev/null +++ b/com/klammertext.rc @@ -0,0 +1,4 @@ +// Resource script for the Windows builds of the commands: embeds +// klammertext.manifest as the application manifest (resource type 24 is +// RT_MANIFEST, id 1 the process manifest). Compiled by windres; see com/Makefile. +1 24 "klammertext.manifest" diff --git a/doc/install/windows_source_install.md b/doc/install/windows_source_install.md new file mode 100644 index 0000000..4756236 --- /dev/null +++ b/doc/install/windows_source_install.md @@ -0,0 +1,305 @@ +# Klammertext source installation on Windows + +This document describes how to build and install Klammertext from source on +Windows 10 or 11, so that the three commands `ktext`, `kdesc` and `kdiag` +run as native Windows programs. Klammertext is developed on Linux and +macOS; Windows is a supported place to *use* it, and installing it there +takes more steps than on those systems, because Windows has no package +manager that supplies a C++ compiler and `make`. The steps below were +followed on a fresh Windows 11 machine and are the ones that worked. + +The installation uses three things a Windows user installs once: +**MSYS2**, which provides the compiler, `make` and a Unix-style shell for +building; **Python** from python.org, which Klammertext embeds; and **TeX +Live**, for PDF output. Everyday use needs only PowerShell. + + +## Prerequisites + +### Git and Python + +In PowerShell (search for "PowerShell" in the Start menu): + +```powershell +winget install --id Git.Git -e +winget install --id Python.Python.3.12 -e --override "/quiet InstallAllUsers=1 PrependPath=1" +``` + +The second command installs Python for all users into `C:\Program +Files\Python312` and puts it on the `Path`. Open a new PowerShell window +(the `Path` of an open one does not change) and install the image library +the `@image` klammer uses: + +```powershell +python -m pip install OpenImageIO +python -c "import OpenImageIO" +``` + +Note that the interpreter is called `python` on Windows. There is no +`python3`; the `python3` that Windows itself offers is a placeholder that +opens the Microsoft Store, and Klammertext does not use it. + +### MSYS2 and the compiler + +Install MSYS2 from https://www.msys2.org/ (or `winget install --id +MSYS2.MSYS2 -e`). It installs into `C:\msys64`. Open the **"MSYS2 UCRT64"** +shell from the Start menu — that one, not "MSYS2 MSYS" or "MINGW64": the +UCRT64 environment shares its C runtime with python.org's Python, which is +what lets the two be linked together. Update it and install the toolchain: + +```bash +pacman -Syu # if it closes the window, reopen the UCRT64 shell and run it again +pacman -S mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-make make \ + mingw-w64-ucrt-x86_64-pkgconf mingw-w64-ucrt-x86_64-dlfcn diffutils +``` + +`diffutils` is only needed to run Klammertext's test suites. + +By default the MSYS2 shell does not see the programs Windows has on its +`Path`, which means it cannot see `git` or `python`. Tell it to, once, in +PowerShell, then reopen the UCRT64 shell: + +```powershell +setx MSYS2_PATH_TYPE inherit +``` + +Verify, in the UCRT64 shell: + +```bash +g++ --version # g++.exe (Rev4, Built by MSYS2 project) 16.x +python --version # Python 3.12.x (python.org's, from the Windows Path) +git --version +``` + + +## Clone the repository + +In the UCRT64 shell. Windows paths are written with forward slashes there, +`/c/Users/` for `C:\Users\`: + +```bash +cd /c/Users/ +git clone https://git.andykopra.com/ack/klammertext.git +cd klammertext +``` + +Git for Windows must not translate line endings: Klammertext's `.kt` +files, `.k` files and shell scripts are LF files. The clone above respects +that as long as `core.autocrlf` is not `true`; check with `git config +--get core.autocrlf` and, if it prints `true`, set it to `false` and clone +again. + + +## Environment variables + +Klammertext's runtime environment is one self-configuring file, the same +one Linux and macOS use. Source it from the UCRT64 shell's profile: + +```bash +echo 'source /c/Users//klammertext/env/runtime.env' >> ~/.bashrc +source ~/.bashrc +``` + +On Windows it sets `KLAMMERTEXT_HOME` and `TMPDIR` in the form Windows +programs understand (`C:/Users//klammertext`), adds `bin/` to `PATH`, +finds a TeX Live installed under `C:\Users\\external\texlive\` +(see below), and sets `PYTHON=python`. Nothing is edited by hand. + + +## Build + +In the UCRT64 shell, in the repository: + +```bash +make -C com -j +``` + +`-j` compiles in parallel; on a machine with 8 GB of memory or less the +optimizing compiler may run out of memory and report `out of memory +allocating`. If so, run `make -C com` without `-j`: slower, and it +finishes. + +This builds the library, the Standard Klammer Set's components and the +three commands into `bin\`, and copies the library and the compiler's +runtime DLLs there too, so `bin\` is self-contained. The Python binding is +built into `kpy\`. Verify: + +```bash +ktext -s '@eval 1 + 1 @' -d # prints 2 +ktext -s '@eval :shell echo hello | tr a-z A-Z @' -d # prints HELLO +``` + +The second line shows that `@eval :shell` runs a POSIX shell on Windows as +it does elsewhere: Klammertext ships one (`win\busybox\`), so a document's +`:shell` command means the same thing on every system. + +For a first document, create `hello.kt`: + +``` +@document +:structure article +:title Hello +:text +@s1 Hello, Klammertext @ + +This document was built from source on Windows. +@ +``` + +and render it: + +```bash +ktext hello.kt -t html # writes hello\index.html +``` + + +## TeX Live (for PDF output) + +The Standard Klammer Set renders PDF with **XeLaTeX**. Install TeX Live's +`scheme-small` plus the packages the SKS needs into +`C:\Users\\external\texlive\2026`, where `runtime.env` finds it. This +is a download of about 600 MB and takes twenty to thirty minutes. + +1. Download `install-tl.zip` from a CTAN mirror, for example + `https://ctan.math.illinois.edu/systems/texlive/tlnet/install-tl.zip`, + and unzip it. It unpacks into a folder named `install-tl-`. + +2. In that folder, create a file `texlive.profile` with this content, + replacing `` with your Windows user name: + + ``` + selected_scheme scheme-small + TEXDIR C:/Users//external/texlive/2026 + TEXMFLOCAL C:/Users//external/texlive/2026/texmf-local + TEXMFSYSCONFIG C:/Users//external/texlive/2026/texmf-config + TEXMFSYSVAR C:/Users//external/texlive/2026/texmf-var + TEXMFHOME C:/Users//texmf + TEXMFVAR C:/Users//.texlive2026/texmf-var + TEXMFCONFIG C:/Users//.texlive2026/texmf-config + tlpdbopt_install_docfiles 0 + tlpdbopt_install_srcfiles 0 + tlpdbopt_autobackup 0 + tlpdbopt_desktop_integration 0 + tlpdbopt_file_assocs 0 + tlpdbopt_w32_multi_user 0 + ``` + +3. In PowerShell, in that folder, run the installer without its graphical + interface, naming the same mirror: + + ```powershell + .\install-tl-windows.bat -no-gui -profile texlive.profile -repository https://ctan.math.illinois.edu/systems/texlive/tlnet + ``` + + Leave the window open until it reports that the installation is + complete. (A warning about `tlaunch.ini` is harmless.) + +4. Still in PowerShell, install the additional packages the SKS requires + beyond `scheme-small` — the same list `doc/install/texlive_additional_packages.sh` + installs on Linux and macOS — and rebuild the formats: + + ```powershell + $tl = "$env:USERPROFILE\external\texlive\2026\bin\windows" + $env:Path = "$tl;$env:Path" + & "$tl\tlmgr.bat" option repository https://ctan.math.illinois.edu/systems/texlive/tlnet + & "$tl\tlmgr.bat" install adjustbox collectbox collection-fontsrecommended enumitem fontaxes footmisc inconsolata layouts mdframed multirow needspace opensans pict2e textpos titlesec upquote zref + & "$tl\fmtutil-sys.exe" --all + ``` + + The second line matters: `tlmgr` and `fmtutil` start helper programs + such as `kpsewhich` by name and find them only through the `Path`. + +Open a new UCRT64 shell (so `runtime.env` runs again and finds the tree) +and verify: + +```bash +xelatex --version +ktext hello.kt -t pdf # writes hello.pdf +``` + + +## Using the commands from PowerShell + +Building needs the UCRT64 shell; using Klammertext does not. For everyday +use from PowerShell, put three directories on the `Path` and set +`KLAMMERTEXT_HOME`, once, as user environment variables (Settings ▸ +System ▸ About ▸ Advanced system settings ▸ Environment Variables, or with +`setx`): + +```powershell +setx KLAMMERTEXT_HOME "C:\Users\\klammertext" +setx Path "$env:Path;C:\Users\\klammertext\bin;C:\Users\\external\texlive\2026\bin\windows" +``` + +Python is already on the `Path` from its installer. Open a new PowerShell +window and `ktext`, `kdesc` and `kdiag` work there directly. Paths +Klammertext prints use forward slashes on every system; paths you type may +use either. + + +## Verifying the installation + +In the UCRT64 shell: + +```bash +ktext -s '@eval 1 + 1 @' -d # Python evaluation +ktext -s '@eval :shell date @' -d # shell evaluation (the bundled sh) +kdesc -t # the targets +kdesc --typeface # the fonts Klammertext ships +ktext hello.kt -t html # html +ktext hello.kt -t pdf # pdf, requires TeX Live + +make -C $KLAMMERTEXT_HOME/tst test # the engine's test suites +make -C $KLAMMERTEXT_HOME/sks/tst test # the Standard Klammer Set's suites (needs TeX Live) +``` + +A few suite cases report `SKIP` on Windows for things the MSYS2 shell +cannot provide, such as a real terminal for the colour tests or a +symbolic link; that is expected. + + +## Updating + +In the UCRT64 shell: + +```bash +cd $KLAMMERTEXT_HOME +git pull +make -C com -j +``` + + +## Troubleshooting + +**The command window closes at once, or "The code execution cannot proceed +because python312.dll was not found".** Python's directory is not on the +`Path` of that window. Reinstall Python with "Add to PATH" selected, or add +`C:\Program Files\Python312` to the `Path` yourself, and open a new window. + +**`import OpenImageIO` fails with "DLL load failed while importing +OpenImageIO".** The OpenImageIO package needs the Microsoft Visual C++ +runtime, which nearly every Windows machine already has but a freshly +installed one may not. Install it from +https://aka.ms/vs/17/release/vc_redist.x64.exe and try the import again. + +**"libklammertext.so was not found" or "libstdc++-6.dll was not found".** +`bin\` is not on the `Path`, or the build did not finish: `make -C com -j` +places the library and the runtime DLLs in `bin\` beside the commands. + +**"The shell Klammertext runs sh commands with on Windows is missing".** +`win\busybox\busybox64u.exe` is part of the repository; the clone is +incomplete or `KLAMMERTEXT_HOME` points elsewhere. + +**"The 'xelatex' command required to make PDF files is not installed".** +TeX Live's `bin\windows` directory is not on the `Path` of the shell you +are using. In the UCRT64 shell `runtime.env` adds it when the tree is under +`C:\Users\\external\texlive\`; in PowerShell add it as shown +above. + +**`git` or `python` are "command not found" in the UCRT64 shell.** +`MSYS2_PATH_TYPE` is not set to `inherit`, or the shell was opened before +you set it. Set it and open a new UCRT64 shell. + +**The build is slow.** Windows Defender scans every file the compiler +writes. Excluding the repository folder from real-time scanning speeds the +build up considerably; it is not required. diff --git a/env/makefile.env b/env/makefile.env index bac46e9..cea6efd 100644 --- a/env/makefile.env +++ b/env/makefile.env @@ -27,9 +27,20 @@ else COMPILER ?= gcc # gcc | clang endif -# Python: version- and platform-agnostic, no hardcoded paths. +# Python: version- and platform-agnostic, no hardcoded paths. On Windows the +# interpreter is python.org's (the one PyPI wheels -- OpenImageIO -- are built +# for); it ships no python3-config, so its prefix is asked of the interpreter +# itself and its import library named by version (libs/python312.lib). +ifneq (,$(filter MINGW% MSYS%,$(UNAME_S))) +PYTHON := python +PYTHON_PREFIX := $(shell cygpath -m "$$($(PYTHON) -c 'import sys; print(sys.base_prefix)')") +PYTHON_INC := -I"$(PYTHON_PREFIX)/Include" +PYTHON_LIB := -L"$(PYTHON_PREFIX)/libs" -l$(shell $(PYTHON) -c 'import sys; print("python%d%d" % sys.version_info[:2])') +else +PYTHON := python3 PYTHON_INC := $(shell python3-config --includes) PYTHON_LIB := $(shell python3-config --ldflags --embed) +endif # Flags common to all platforms. CPPFLAGS = -I$(KLAMMERTEXT_HOME)/mac $(PYTHON_INC) @@ -52,6 +63,29 @@ SHARED = -dynamiclib SONAME = -install_name @rpath/$(notdir $@) ORIGIN := @loader_path EXPORT_DYNAMIC = -Wl,-export_dynamic +else ifneq (,$(filter MINGW% MSYS%,$(UNAME_S))) +# ---------- Windows (MSYS2, UCRT64 environment; native PE binaries) ---------- +# uname says MINGW64_NT-... under UCRT64 too. The compilers come from the +# environment's PATH (/ucrt64/bin). libdl.a is the mingw-w64-dlfcn shim over +# LoadLibrary, linked statically so no libdl.dll travels with the products. +# PE has no soname and no rpath; a DLL is found through PATH or +# the executable's own directory, and GNU ld's -l search list on PE does not +# include ".so", hence the exact-name form below. +GCC := g++ +CLANG := clang++ +LDFLAGS = +LDLIBS = -l:libdl.a $(if $(NOPYTHON),,$(PYTHON_LIB)) +SHARED = -shared +SONAME = +ORIGIN := $$ORIGIN +EXPORT_DYNAMIC = -Wl,--export-all-symbols +KLAMMERTEXT_LIB = -l:libklammertext.so +# A Windows program finds a DLL through PATH or its own directory, never an +# rpath. So every directory that holds a program or a Python extension also +# holds the DLLs it needs: libklammertext.so and the three gcc runtime DLLs +# the toolchain links (from the compiler's own bin/). com/ and kpy/ copy them. +GCC_BIN := $(dir $(shell cygpath -m "$$(command -v g++)")) +GCC_RUNTIME_DLLS := libstdc++-6.dll libgcc_s_seh-1.dll libwinpthread-1.dll else # ---------- Linux ---------- GCC := /usr/bin/g++ @@ -63,6 +97,8 @@ SONAME = -Wl,-soname,$(notdir $@) ORIGIN := $$ORIGIN EXPORT_DYNAMIC = -rdynamic endif +# How a link line names libklammertext (set above for Windows). +KLAMMERTEXT_LIB ?= -lklammertext # Resolve the compiler choice (gcc by default; clang with COMPILER=clang). CXX := $(if $(filter clang,$(COMPILER)),$(CLANG),$(GCC)) diff --git a/env/runtime.env b/env/runtime.env index f94ca74..3752a22 100644 --- a/env/runtime.env +++ b/env/runtime.env @@ -20,6 +20,19 @@ export KLAMMERTEXT_HOME="$(cd "$(dirname "$_kt_self")/.." && pwd)" unset _kt_self _kt_uname="$(uname -s)" +# Windows under MSYS2: the commands are native programs and never understand +# MSYS's /c/... spelling, so KLAMMERTEXT_HOME and TMPDIR reach them in Windows +# form (C:/..., cygpath -m). bash's own PATH keeps the POSIX form -- a colon +# inside a PATH entry splits it. Python (python.org's) is the user's to put on +# PATH; an MSYS2 login shell does not inherit the Windows PATH by default. +_kt_home_posix="$KLAMMERTEXT_HOME" +case "$_kt_uname" in + MINGW*|MSYS*) + export KLAMMERTEXT_HOME="$(cygpath -m "$KLAMMERTEXT_HOME")" + export TMPDIR="$(cygpath -m "${TMPDIR:-/tmp}")" + export PYTHON="${PYTHON:-python}" # python.org installs no python3 + ;; +esac # --- KLAMMERTEXT_TEXLIVE_BIN: newest installed TeX Live under ~/external ------- # Klammertext's TeX Live installs are named by 4-digit year (2024, 2025, 2026). @@ -27,20 +40,27 @@ _kt_uname="$(uname -s)" # are never selected; sort -V then picks the newest year. if [ "$_kt_uname" = "Darwin" ]; then _kt_arch="universal-darwin" +elif case "$_kt_uname" in MINGW*|MSYS*) true;; *) false;; esac; then + _kt_arch="windows" # TeX Live's bin/windows else _kt_arch="$(uname -m)-linux" # e.g. x86_64-linux fi # Use `find` with a -path pattern, NOT a shell glob: on macOS/zsh an unmatched # glob (no texlive installed) is a hard "no matches found" error, whereas find # just returns nothing. The -path pattern keeps the 4-digit-year restriction. -_kt_tl="$(find "$HOME/external/texlive" -maxdepth 3 -type d \ +# Windows: a user installs under the Windows home, %USERPROFILE%\external +# (MSYS2's $HOME is C:\msys64\home\, which no Windows guide would name). +_kt_tlroot="$HOME/external/texlive" +case "$_kt_uname" in MINGW*|MSYS*) _kt_tlroot="$(cygpath -u "$USERPROFILE")/external/texlive";; esac +_kt_tl="$(find "$_kt_tlroot" -maxdepth 3 -type d \ -path "*/[0-9][0-9][0-9][0-9]/bin/$_kt_arch" 2>/dev/null \ | sort -V | tail -1)" [ -n "$_kt_tl" ] && export KLAMMERTEXT_TEXLIVE_BIN="$_kt_tl" -unset _kt_tl _kt_arch +unset _kt_tl _kt_arch _kt_tlroot # --- PATH (TeX Live appended only if one was found) --------------------------- -export PATH="$KLAMMERTEXT_HOME/bin:$KLAMMERTEXT_HOME/tst${KLAMMERTEXT_TEXLIVE_BIN:+:$KLAMMERTEXT_TEXLIVE_BIN}:$PATH" +export PATH="$_kt_home_posix/bin:$_kt_home_posix/tst${KLAMMERTEXT_TEXLIVE_BIN:+:$KLAMMERTEXT_TEXLIVE_BIN}:$PATH" +unset _kt_home_posix # --- The Python binding (kpy/) ----------------------------------------------- # "import klammertext" with no setup, as the container image already arranges diff --git a/kpy/Makefile b/kpy/Makefile index fb93ec4..e26353b 100644 --- a/kpy/Makefile +++ b/kpy/Makefile @@ -21,7 +21,7 @@ include $(K)/env/makefile.env LIBDIR := $(K)/lib LIBRARY := $(LIBDIR)/libklammertext.so -EXT_SUFFIX := $(shell python3-config --extension-suffix) +EXT_SUFFIX := $(shell $(PYTHON) -c 'import sysconfig; print(sysconfig.get_config_var("EXT_SUFFIX"))') MODULE := klammertext$(EXT_SUFFIX) # "../lib": the module sits in $(K)/kpy, one level below $(K). @@ -34,7 +34,16 @@ DEPFLAGS = -MMD -MP -MF $(@:.o=.d) .PHONY: all clean -all : $(MODULE) +# Windows: the module's dependent DLLs sit beside it (Python loads an +# extension's dependencies from the extension's own directory, never from +# PATH or an rpath -- there is none on PE); see makefile.env. +ifneq (,$(filter MINGW% MSYS%,$(UNAME_S))) +BESIDE_DLLS := libklammertext.so $(GCC_RUNTIME_DLLS) +else +BESIDE_DLLS := +endif + +all : $(MODULE) $(BESIDE_DLLS) %.o : %.cpp @echo Compiling $< @@ -42,10 +51,17 @@ all : $(MODULE) $(MODULE) : $(OBJECTS) $(LIBRARY) $(CXX) $(CPPFLAGS) $(CXXFLAGS) $(KPY_LDFLAGS) $(LDFLAGS) \ - -o $@ $(OBJECTS) -lklammertext $(LDLIBS) + -o $@ $(OBJECTS) $(KLAMMERTEXT_LIB) $(LDLIBS) + +ifneq (,$(filter MINGW% MSYS%,$(UNAME_S))) +libklammertext.so : $(LIBRARY) + cp $< $@ +%.dll : $(GCC_BIN)%.dll + cp $< $@ +endif clean : - rm -f $(OBJECTS) $(DEPFILES) klammertext.*.so *~ + rm -f $(OBJECTS) $(DEPFILES) klammertext.*.so klammertext.*.pyd libklammertext.so *.dll *~ rm -rf __pycache__ -include $(DEPFILES) diff --git a/kpy/klammertext.cpp b/kpy/klammertext.cpp index c1b521b..a4f4a3b 100644 --- a/kpy/klammertext.cpp +++ b/kpy/klammertext.cpp @@ -215,9 +215,12 @@ int instances = 0; // module, so a relative @read in read() text resolves against the directory // the script runs in -- the same rule ktext applies to -s input, and set at // each read() because a script may chdir between two of them. +// The command's identity, through the library (set_command_identity): on +// Windows an assignment to error.h's inline variables from this module +// would land in this module's own copies, not the library's. void set_string_source() { - command_pathname = (fs::current_path() / "klammertext").string(); + set_command_identity("klammertext", (fs::current_path() / "klammertext").generic_string()); } PyObject* Machine_new(PyTypeObject* type, PyObject*, PyObject*) @@ -607,7 +610,6 @@ PyMODINIT_FUNC PyInit_klammertext(void) return nullptr; } // What an error report names as the command. - command_name = "klammertext"; set_string_source(); const char* home = std::getenv(klammertext_home_var.c_str()); PyObject* home_obj = home ? PyUnicode_FromString(home) : Py_None; diff --git a/mac/argv.h b/mac/argv.h index 3338bf7..6433fb2 100644 --- a/mac/argv.h +++ b/mac/argv.h @@ -134,5 +134,5 @@ public: // these, so a shell-quoted filename containing spaces stays one // element; the space-joined m_value remains only for get()/describe(). std::map> m_vectors {}; - long unsigned int m_syntax_size = 0; + std::size_t m_syntax_size = 0; }; diff --git a/mac/command.cpp b/mac/command.cpp index 87442ce..238b691 100644 --- a/mac/command.cpp +++ b/mac/command.cpp @@ -1,3 +1,7 @@ +#ifdef _WIN32 +#include +#include +#endif #include #include "argv.h" @@ -21,11 +25,24 @@ fs::path construct_command_pathname(char* command) return fs::current_path() / fs::path(command).filename(); } +void set_command_identity(const std::string& name, const std::string& pathname) +{ + command_name = name; + command_pathname = pathname; +} + void set_verbose_level(int argc, char* argv[]) { +#ifdef _WIN32 + // Klammertext writes LF on every system: a document a Windows user pipes + // or redirects must not gain CR, and the suites read the streams byte for + // byte. The C runtime's text mode would append it to every line. + _setmode(_fileno(stdout), _O_BINARY); + _setmode(_fileno(stderr), _O_BINARY); +#endif //command_name = absolute_pathname(argv[0]); - command_name = std::string(argv[0]); - command_pathname = construct_command_pathname(argv[0]); + command_name = fs::path(argv[0]).generic_string(); + command_pathname = construct_command_pathname(argv[0]).generic_string(); //std::cout << "set_verbose_level: " << command_pathname << "\n"; Argv args {}; @@ -140,7 +157,7 @@ void set_command_state( if (!input_filenames.empty()) { S.set("K_input_dir", absolute_pathname(file_directory(input_filenames[0]))); } else { - S.set("K_input_dir", fs::current_path().string()); + S.set("K_input_dir", fs::current_path().generic_string()); } } @@ -178,7 +195,7 @@ void load_klammersets(Machine& machine, const strings_t& symbols) continue; } std::string klammerset_filename = resolve_klammerset_symbol( - symbol, machine.m_state.value("K_input_dir"), Locator::none()).string(); + symbol, machine.m_state.value("K_input_dir"), Locator::none()).generic_string(); // Symbol -> file is the search path's answer, and the search // path has three stages with shadowing: the file it landed on is // exactly what a user cannot read off "--klammersets x". diff --git a/mac/command.h b/mac/command.h index 35fb21d..9299e44 100644 --- a/mac/command.h +++ b/mac/command.h @@ -10,6 +10,13 @@ class Machine; void set_verbose_level(int argc, char* argv[]); +// What an error report and the -s pseudo source name as the command, set +// from OUTSIDE the library (the Python binding). It is a function, not an +// assignment to the header's inline variables, because on Windows every +// module -- the DLL, an extension module -- has its OWN copy of an inline +// variable: an assignment made in the binding never reached the copy the +// library's error reports read. On Linux the two are one variable. +void set_command_identity(const std::string& name, const std::string& pathname); bool show_usage(int argc, char* argv[]); fs::path construct_command_pathname(char* command); diff --git a/mac/coverage.cpp b/mac/coverage.cpp index a480cd1..3b29ae1 100644 --- a/mac/coverage.cpp +++ b/mac/coverage.cpp @@ -139,8 +139,10 @@ std::string short_path(const std::string& path) std::error_code ec {}; fs::path home_real = fs::weakly_canonical(fs::path(home), ec); fs::path path_real = fs::weakly_canonical(fs::path(path), ec); - std::string prefix = (ec ? std::string(home) : home_real.string()) + "/"; - const std::string& subject = ec ? path : path_real.string(); + // Both in the generic spelling ('/'): on Windows weakly_canonical + // answers with backslashes while the report's paths use '/'. + std::string prefix = (ec ? fs::path(home).generic_string() : home_real.generic_string()) + "/"; + const std::string subject = ec ? fs::path(path).generic_string() : path_real.generic_string(); if (subject.size() > prefix.size() && subject.compare(0, prefix.size(), prefix) == 0) { return subject.substr(prefix.size()); diff --git a/mac/error.cpp b/mac/error.cpp index e97c192..f666b16 100644 --- a/mac/error.cpp +++ b/mac/error.cpp @@ -11,7 +11,7 @@ bool display_source(const std::string& filename_arg) return false; } else { fs::path filename = fs::path(filename_arg).filename(); - return sks_commands.count(filename) == 0; + return sks_commands.count(filename.string()) == 0; } } diff --git a/mac/eval.cpp b/mac/eval.cpp index 3b0a0b4..4aceb51 100644 --- a/mac/eval.cpp +++ b/mac/eval.cpp @@ -10,7 +10,16 @@ #include #include #include +#ifdef _WIN32 +// mingw-w64 has no : there pclose() returns the child's exit code +// itself, not a wait status, and no signal can have killed it. +#define WIFEXITED(s) ((s) != -1) +#define WEXITSTATUS(s) (s) +#define WIFSIGNALED(s) 0 +#define WTERMSIG(s) 0 +#else #include +#endif // Run a shell command and return its STANDARD OUTPUT, which becomes document // text. Two things the first version did not do, both of them the same defect @@ -51,7 +60,8 @@ std::string shell(State state, std::string command, Locator loc) // The braces keep the redirection outside the writer's command, so a // command containing its own pipeline or redirection still works. std::string wrapped = "{ " + command + " ; } 2>" + q_(err_path); - FILE* pipe = popen(wrapped.c_str(), "r"); + Shell_command sh_cmd(wrapped, loc); + FILE* pipe = popen(sh_cmd.line().c_str(), "r"); if (!pipe) { fs::remove(err_path); throw Environment_error( @@ -129,7 +139,9 @@ std::string run_haskell(const std::string& hsfile, Locator loc) err_path = err_template.data(); std::string command = "runghc " + q_(hsfile) + " 2>" + q_(err_path); - FILE* pipe = popen(command.c_str(), "r"); + // Through the same sh as :shell: the quoting is sh quoting. + Shell_command sh_cmd(command, loc); + FILE* pipe = popen(sh_cmd.line().c_str(), "r"); if (!pipe) { fs::remove(err_path); throw Environment_error("Could not run runghc.", loc); diff --git a/mac/file.cpp b/mac/file.cpp index 8a16bc5..4d11ff8 100644 --- a/mac/file.cpp +++ b/mac/file.cpp @@ -33,7 +33,7 @@ std::string absolute_pathname(const std::string& filename, const std::string& ba { fs::path p(filename); if (filename.empty()) { - return fs::current_path().string(); + return fs::current_path().generic_string(); } if (!base.empty()) { // Resolve filename relative to base's directory @@ -41,7 +41,7 @@ std::string absolute_pathname(const std::string& filename, const std::string& ba fs::path base_dir = fs::is_directory(b) ? b : b.parent_path(); p = base_dir / p; } - return fs::absolute(p).string(); + return fs::absolute(p).generic_string(); } std::string relative_pathname(const std::string& filename) @@ -70,6 +70,66 @@ size_t count_substrings(const std::string& text, const std::string& substring) { return count; } +std::string home_directory() +{ + if (const char* home = std::getenv("HOME"); home && *home) { + return home; + } +#ifdef _WIN32 + if (const char* profile = std::getenv("USERPROFILE"); profile && *profile) { + return profile; + } +#endif + return {}; +} + +std::string find_in_path(const std::string& program) +{ + const char* path = std::getenv("PATH"); + if (path == nullptr) { + return {}; + } + std::stringstream ss(path); + std::string dir; + while (std::getline(ss, dir, path_list_separator)) { + if (dir.empty()) { + continue; + } + fs::path candidate = fs::path(dir) / program; + std::error_code ec {}; + if (fs::is_regular_file(candidate, ec)) { + return candidate.generic_string(); + } +#ifdef _WIN32 + fs::path exe = candidate; + exe += ".exe"; + if (fs::is_regular_file(exe, ec)) { + return exe.generic_string(); + } +#endif + } + return {}; +} + +std::vector directory_list(const char* variable, const std::string& default_subdir) +{ + std::string paths {}; + if (const char* env = std::getenv(variable); env && *env) { + paths = env; + } else if (std::string home = home_directory(); !home.empty()) { + paths = home + "/" + default_subdir; + } + std::vector result {}; + std::stringstream ss(paths); + std::string dir; + while (std::getline(ss, dir, path_list_separator)) { + if (!dir.empty()) { + result.push_back(dir); + } + } + return result; +} + fs::path relative_to_cwd(const fs::path& input) { const auto base = fs::current_path(); @@ -289,8 +349,10 @@ std::string string_from_file(const std::string& pathname, bool strip_surrounding void string_to_file(const std::string& pathname, std::string contents) { - fs::create_directories(file_directory(fs::absolute(pathname))); - std::ofstream out(pathname); + fs::create_directories(file_directory(fs::absolute(pathname).string())); + // Binary: the text Klammertext writes ends its lines with LF on every + // system; Windows' text mode would turn each into CRLF. + std::ofstream out(pathname, std::ios::binary); if (!out) { throw File_error("Could not write file " + pathname); } @@ -319,7 +381,7 @@ strings_t get_files_in_directory(const std::string& dir) for (const auto& entry : fs::directory_iterator(dir)) { if (entry.is_regular_file()) { // std::cout << entry.path().filename() << std::endl; - result.push_back(entry.path().filename()); + result.push_back(entry.path().filename().string()); } } } catch (const fs::filesystem_error& ex) { @@ -456,8 +518,11 @@ strings_t get_sks_directories(const std::string& s, bool include_argument) std::string symbolic_klammertext_filename(std::string filename) { + // Both in the generic spelling: a Windows KLAMMERTEXT_HOME may be written + // with either separator, and a path Klammertext prints uses '/'. return string_replace( - filename, get_env_var("KLAMMERTEXT_HOME"), "$KLAMMERTEXT_HOME"); + fs::path(filename).generic_string(), + fs::path(get_env_var("KLAMMERTEXT_HOME")).generic_string(), "$KLAMMERTEXT_HOME"); } diff --git a/mac/file.h b/mac/file.h index d7211d7..522aba2 100644 --- a/mac/file.h +++ b/mac/file.h @@ -16,6 +16,28 @@ std::string relative_pathname(const std::string& filename, const std::string& ba fs::path relative_to_cwd(const fs::path& input); +// The separator of a directory list in an environment variable +// (KLAMMERTEXT_FONTS, KLAMMERTEXT_KLAMMERSETS): ';' on Windows, where a drive +// letter puts a colon inside every absolute path, ':' everywhere else -- the +// convention of PATH on each system. +#ifdef _WIN32 +constexpr char path_list_separator = ';'; +#else +constexpr char path_list_separator = ':'; +#endif +// The user's home directory as the system names it: HOME, and on Windows +// USERPROFILE when HOME is unset (a plain PowerShell or cmd sets no HOME; +// an MSYS2 shell does). Empty when neither is set. +std::string home_directory(); +// The first PROGRAM on PATH (split on path_list_separator; on Windows the +// name also tried with ".exe"), or "" when none: what "which" answers, without +// spawning a shell -- on Windows popen()'s cmd.exe has no "which". +std::string find_in_path(const std::string& program); +// The directories listed in an environment variable, split on +// path_list_separator, empty entries dropped; DEFAULT_SUBDIR under the home +// directory when the variable is unset or empty. +std::vector directory_list(const char* variable, const std::string& default_subdir); + bool file_exists(const std::string& pathname, bool error_if_not=false, bool is_directory=false); // Text to its lines, and a file to (its whole text, its lines). These diff --git a/mac/font_store.cpp b/mac/font_store.cpp index 1d9cdb3..9f32580 100644 --- a/mac/font_store.cpp +++ b/mac/font_store.cpp @@ -23,12 +23,20 @@ namespace fs = std::filesystem; +// The interpreter spawned for the font-metric script: python.org's Windows +// build installs "python" only; Linux and macOS name it "python3". +#ifdef _WIN32 +static const char* const python_interpreter = "python"; +#else +static const char* const python_interpreter = "python3"; +#endif + // A filesystem extension lowercased for case-insensitive matching, so a // font named Foo.TTF or Foo.OTF is recognized like Foo.ttf / Foo.otf. static std::string lower_extension(const fs::path& path) { - std::string ext = path.extension(); + std::string ext = path.extension().string(); for (char& c : ext) { c = std::tolower(static_cast(c)); } @@ -49,29 +57,14 @@ std::string name_to_dirname(const std::string& name) } -// The directories searched for installed fonts: the colon-separated -// KLAMMERTEXT_FONTS environment variable, or ~/.klammertext/fonts when it -// is not set. Directories are searched in listed order, and the default -// fonts in $KLAMMERTEXT_HOME/fnt are searched last, so an installed font -// can deliberately shadow a default one. +// The directories searched for installed fonts: the KLAMMERTEXT_FONTS +// environment variable (a list in the system's PATH convention: ':' or, on +// Windows, ';'), or ~/.klammertext/fonts when it is not set. Directories are +// searched in listed order, and the default fonts in $KLAMMERTEXT_HOME/fnt are +// searched last, so an installed font can deliberately shadow a default one. strings_t installed_font_dirs() { - strings_t result {}; - std::string paths {}; - const char* env = std::getenv("KLAMMERTEXT_FONTS"); - if (env && *env) { - paths = env; - } else if (const char* home = std::getenv("HOME"); home && *home) { - paths = std::string(home) + "/.klammertext/fonts"; - } - std::stringstream ss(paths); - std::string dir; - while (std::getline(ss, dir, ':')) { - if (!dir.empty()) { - result.push_back(dir); - } - } - return result; + return directory_list("KLAMMERTEXT_FONTS", ".klammertext/fonts"); } std::string default_font_dir() @@ -151,7 +144,7 @@ std::vector fonts_in_directory(const std::string& base) for (auto& entry : fs::directory_iterator(base)) { if (entry.path().extension() == ".css" && fs::is_directory(base + "/" + entry.path().stem().string())) { - names.push_back(entry.path().stem()); + names.push_back(entry.path().stem().string()); } } std::sort(names.begin(), names.end()); @@ -199,10 +192,12 @@ static void extract_font_metrics(Resolved_font& font) if (!fs::exists(ttf_path)) return; // Extract both x-height and cap-height ratios from OS/2 table + // The path goes inside a Python literal: forward slashes, which Python + // opens on every system, where a Windows backslash would be an escape. std::string script = - "python3 -c \"" + std::string(python_interpreter) + " -c \"" "import struct; " - "f = open('" + ttf_path + "', 'rb'); " + "f = open('" + fs::path(ttf_path).generic_string() + "', 'rb'); " "_, n = struct.unpack('>IH', f.read(6)); " "f.read(6); " "t = {};\n" @@ -219,6 +214,9 @@ static void extract_font_metrics(Resolved_font& font) "print(f'{xh/upm:.4f} {ch/upm:.4f}') if ver >= 2 else None; " "f.close()\""; std::string result = trim(exec(script.c_str())); + // A DERIVED value a designer cannot read off the font: the two ratios + // (or nothing, when the script failed) decide the html size normalization. + (void)K::log(3, "Font metrics of " + ttf_path + ": " + (result.empty() ? std::string("(none)") : result)); if (!result.empty()) { try { auto pos = result.find(' '); @@ -418,7 +416,7 @@ std::vector classify_font_files(const std::string& directory) for (auto& entry : fs::recursive_directory_iterator(directory)) { std::string ext = lower_extension(entry.path()); if (entry.is_regular_file() && (ext == ".ttf" || ext == ".otf")) { - result.push_back(classify_font_file(entry.path())); + result.push_back(classify_font_file(entry.path().string())); } } return result; @@ -521,7 +519,7 @@ std::string write_font_samples(const std::string& output_dir, const std::string& install_fonts(source_dir, output_dir + "/fonts"); for (auto& entry : fs::directory_iterator(output_dir + "/fonts")) { if (entry.path().extension() == ".css") { - std::string css = string_from_file(entry.path()); + std::string css = string_from_file(entry.path().string()); std::smatch match {}; if (std::regex_search(css, match, std::regex(R"(font-family:\s*'([^']+)')"))) { diff --git a/mac/katomize.cpp b/mac/katomize.cpp index 5374473..558af9f 100644 --- a/mac/katomize.cpp +++ b/mac/katomize.cpp @@ -160,3 +160,8 @@ void warn_unparsed_katoms(katom_list& katoms, bool warn) } } } + +void set_show_rewrite_rules(bool on) +{ + show_rewrite_rules = on; +} diff --git a/mac/katomize.h b/mac/katomize.h index 85bf4c5..314639a 100644 --- a/mac/katomize.h +++ b/mac/katomize.h @@ -82,6 +82,10 @@ katom_list katomize_lines(const strings_t& lines, const std::string& source, // expand_compound_katom() and attributed to whichever rule the loop // happened to be holding, so it always read as "bar precedence". inline bool show_rewrite_rules = false; +// Set it through this, never by assignment from a command: on Windows the +// command's copy of an inline variable is not the library's (see +// set_command_identity in command.h). +void set_show_rewrite_rules(bool on); // Warn about text that matched no katom type and that no rule could divide // -- but only where it survives processing: text removed by #, ##, or diff --git a/mac/klammerset_registry.cpp b/mac/klammerset_registry.cpp index a07f570..a65ac88 100644 --- a/mac/klammerset_registry.cpp +++ b/mac/klammerset_registry.cpp @@ -151,7 +151,7 @@ std::vector klammerset_search_dirs(const std::string& local_dir) // A repeated directory (e.g. the local stage already IS // $KLAMMERTEXT_HOME) adds nothing and clutters error messages. auto push_unique = [&result](const std::string& dir) { - std::string canonical = fs::weakly_canonical(dir).string(); + std::string canonical = fs::weakly_canonical(dir).generic_string(); if (!is_in(canonical, result)) { result.push_back(canonical); } @@ -171,22 +171,10 @@ std::vector klammerset_search_dirs(const std::string& local_dir) // a document could extend its own search path between one declaration and // the next. Nothing intended that -- it fell out of a fresh getenv and an // in-process interpreter. - static const std::string paths = [] { - const char* env = std::getenv("KLAMMERTEXT_KLAMMERSETS"); - if (env && *env) { - return std::string(env); - } - if (const char* home = std::getenv("HOME"); home && *home) { - return std::string(home) + "/.klammertext/klammersets"; - } - return std::string(); - }(); - std::stringstream ss(paths); - std::string dir; - while (std::getline(ss, dir, ':')) { - if (!dir.empty()) { - push_unique(dir); - } + static const strings_t installed = + directory_list("KLAMMERTEXT_KLAMMERSETS", ".klammertext/klammersets"); + for (const std::string& dir : installed) { + push_unique(dir); } if (const char* kthome = std::getenv(klammertext_home_var.c_str()); kthome && *kthome) { push_unique(kthome); @@ -280,11 +268,11 @@ std::string describe_klammerset_search(const std::string& local_dir, int margin) if (!is_klammerset_symbol(symbol) || !fs::is_regular_file(declaration)) { continue; } - std::string line = tab + symbol + sp_arrow + declaration.string(); + std::string line = tab + symbol + sp_arrow + declaration.generic_string(); if (first_hit.count(symbol)) { line += " (shadowed by " + first_hit[symbol] + ")"; } else { - first_hit[symbol] = declaration.string(); + first_hit[symbol] = declaration.generic_string(); } // What a set IS, under where it lives: the symbol and the path // say which file would load, and nothing at all about whether diff --git a/mac/locator.cpp b/mac/locator.cpp index beb5e1b..c009d44 100644 --- a/mac/locator.cpp +++ b/mac/locator.cpp @@ -12,14 +12,14 @@ std::string abbreviate_location( { std::string result = location; fs::path basename = fs::path(location).filename(); - if (sks_commands.contains(basename)) { - result = basename; + if (sks_commands.contains(basename.string())) { + result = basename.string(); } else { if (make_map) { if (relpath.count(location)) { result = relpath[location]; } else { - result = fs::relative(location, fs::current_path()); + result = fs::relative(location, fs::current_path()).generic_string(); relpath[location] = result; } } @@ -29,7 +29,7 @@ std::string abbreviate_location( Locator::Locator(const fs::path& filename, int line, int chr) - : m_filename(filename) + : m_filename(filename.generic_string()) , m_line(line) , m_chr(chr) { @@ -49,7 +49,7 @@ std::string Locator::str(bool relative) const // relative_pathname(m_filename, fs::current_path().string()) << ":" std::string filename = m_filename; if (relative) { - filename = relative_to_cwd(m_filename); + filename = relative_to_cwd(m_filename).generic_string(); } std::stringstream ss {}; ss << "[" << filename << ":" @@ -63,7 +63,7 @@ std::string Locator::desc(bool relative) const // relative_pathname(m_filename, fs::current_path().string()) << ":" std::string filename = m_filename; if (relative) { - filename = relative_to_cwd(m_filename); + filename = relative_to_cwd(m_filename).generic_string(); } std::stringstream ss {}; ss << filename << ", line " << m_line << ", character " << m_chr + 1; diff --git a/mac/locator.h b/mac/locator.h index c64866e..b8222b1 100644 --- a/mac/locator.h +++ b/mac/locator.h @@ -19,7 +19,7 @@ public: // toolchains (e.g. Homebrew GCC on macOS); guard against // fs::path(nullptr) -> strlen(NULL). : m_filename(location.file_name() - ? fs::absolute(location.file_name()) : fs::path{}) + ? fs::absolute(location.file_name()).generic_string() : std::string{}) , m_line(int(location.line())) , m_chr(int(location.column())) {}; diff --git a/mac/machine.cpp b/mac/machine.cpp index 53fefe6..a139c44 100644 --- a/mac/machine.cpp +++ b/mac/machine.cpp @@ -491,7 +491,7 @@ void Machine::read(const fs::path& pathname) // :cpp libraries live next to the file that uses them). m_state.add_search_dir(fs::absolute(pathname).parent_path().string()); m_state.open_frame("Machine state: " + pathname.string()); - input(string_from_file(pathname), pathname.string()); + input(string_from_file(pathname.string()), pathname.generic_string()); m_sources.push_back(pathname); } @@ -519,7 +519,7 @@ void Machine::expand_read_katoms( bool klammers, bool eval, bool cond, bool read) { (void)K::log(3); - current_filename = resolve_relative_to(current_filename); + current_filename = resolve_relative_to(current_filename).generic_string(); // msg() << "current_filename: " << current_filename << "\n"; if (std::find_if(katoms.begin(), katoms.end(), begin_read) != katoms.end()) { (void)K::log(3); @@ -553,11 +553,11 @@ void Machine::expand_read_katoms( input_filename = fs::canonical(input_filename); m_state.add_search_dir(input_filename.parent_path().string()); std::string text = trim_right(string_from_file(input_filename.string())); - katom_list ks = katomize_lines(line_split(text), input_filename); + katom_list ks = katomize_lines(line_split(text), input_filename.generic_string()); // ks = process_katoms( // ks, command_pathname, - ks, input_filename, + ks, input_filename.generic_string(), nonascii, literal, ignore, whitespace, klammers, eval, cond, read); katoms.insert(end, ks.begin(), ks.end()); } @@ -640,7 +640,7 @@ void Machine::load_klammerset_files(const Klammerset& klammerset, katom_iter ins std::vector filenames {}; for (const auto& required : klammerset.m_requires) { std::string path = is_klammerset_symbol(required) - ? resolve_klammerset_symbol(required, base_dir, klammerset.m_loc).string() + ? resolve_klammerset_symbol(required, base_dir, klammerset.m_loc).generic_string() : required; // ALREADY LOADED? Decided BEFORE the file is opened. The guard used // to sit in Klammerset_registry::add, which only runs once the file has @@ -682,11 +682,11 @@ void Machine::load_klammerset_files(const Klammerset& klammerset, katom_iter ins klammerset.m_loc); } pathname = fs::canonical(pathname); - loaded_files.push_back(pathname.string()); + loaded_files.push_back(pathname.generic_string()); m_state.add_search_dir(pathname.parent_path().string()); std::string text = trim_right(string_from_file(pathname.string())); - katom_list ks = katomize_lines(line_split(text), pathname); - process_katoms(ks, pathname); + katom_list ks = katomize_lines(line_split(text), pathname.generic_string()); + process_katoms(ks, pathname.generic_string()); loaded.insert(loaded.end(), ks.begin(), ks.end()); } m_katoms.insert(insert_at, loaded.begin(), loaded.end()); diff --git a/mac/state.cpp b/mac/state.cpp index 9932b27..ab29f83 100644 --- a/mac/state.cpp +++ b/mac/state.cpp @@ -357,7 +357,9 @@ std::string State::python_code() m_search_dirs.begin(), m_search_dirs.end()); for (const auto& d : python_dirs) { if (directory_has_python_file(d)) { - ss << "sys.path.append('" << d << "')\n"; + // A Python string literal, not the raw text: a Windows directory + // holds backslashes, and C:\Users reads as a \U unicode escape. + ss << "sys.path.append(" << pyformat_string({d}) << ")\n"; } } if (!m_frames.empty()) { diff --git a/mac/util.cpp b/mac/util.cpp index 5b47c85..ce894e0 100644 --- a/mac/util.cpp +++ b/mac/util.cpp @@ -1,3 +1,8 @@ +#include +#include +#include "error.h" +#include "file.h" +#include "locator.h" #include #include #include @@ -621,11 +626,51 @@ std::string freplace(const std::string& src, const std::regex& pattern, return result; } +// Shell_command -- see util.h. On Windows the script goes to a file under +// the temp directory (mkstemp, so concurrent runs do not collide), and the +// line is """ sh "