Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ghostscript fails to crop PDF on NixOS #67

Open
0nyr opened this issue Jan 7, 2025 · 1 comment
Open

Ghostscript fails to crop PDF on NixOS #67

0nyr opened this issue Jan 7, 2025 · 1 comment

Comments

@0nyr
Copy link

0nyr commented Jan 7, 2025

Description:

I am encountering an issue when using MakieTeX.jl on NixOS. The library fails to run Ghostscript to crop PDFs due to the specific way NixOS handles dynamically linked executables. Despite attempts to set the correct Ghostscript path, the library seems to continue using the bundled Ghostscript_jll instead of the custom path.


Steps to Reproduce:

  1. Install MakieTeX.jl and its dependencies on NixOS. For instance here is my current flake.nix file:
{
  description = "Julia environment";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
    flake-utils.url = github:numtide/flake-utils;
  };

  outputs = { self, nixpkgs, flake-utils, ... }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        # Override the Nix package set to allow unfree packages
        pkgs = import nixpkgs {
          system = system; 
          config.allowUnfree = true; 
        };

        # julia = pkgs.julia.withPackages [
        #   "LanguageServer"
        #   [...]
        # ];
        # WARN: Above doesn't support all packages, so rely on Julia package manager instead.
        # Use Julia in REPL mode, then package mode and install packages that way.
        julia = pkgs.julia-bin.overrideDerivation (oldAttrs: { doInstallCheck = false; });
      in
      {
        # development environment
        devShells.default = pkgs.mkShell {
          packages = [
            julia
            #wrappedGhostscript
            pkgs.ghostscript

            # OpenGL dependencies
            pkgs.libGL
            pkgs.libglvnd
            pkgs.glfw
            pkgs.mesa
            pkgs.mesa.drivers

            # Font rendering
            pkgs.fontconfig
            pkgs.freetype

            # LaTeX
            pkgs.texlive.combined.scheme-full
            pkgs.biber
            pkgs.gnumake

            # C/C++ development
            pkgs.gcc
            pkgs.gnumake
            pkgs.gdb
            pkgs.valgrind

            pkgs.gurobi # Gurobi solver. Requires a license. Add it using command `grbgetkey <YOUR_LICENSE_KEY>`

            # Java
            pkgs.jdk23
          ];

          shellHook = ''
            export JULIA_NUM_THREADS="auto"
            export JULIA_PROJECT="turing"
            export LD_LIBRARY_PATH=${pkgs.gurobi}/lib:$LD_LIBRARY_PATH
            export GUROBI_HOME=${pkgs.gurobi}
            export GUROBI_VERSION=$(basename $(ls -d ${pkgs.gurobi}) | sed 's/.*-\([0-9]\+\)\.\([0-9]\+\).*/\1\2/')
            export LD_LIBRARY_PATH="/run/opengl-driver/lib:/run/opengl-driver-32/lib:$LD_LIBRARY_PATH"
            export GHOSTSCRIPT_PATH="${pkgs.ghostscript}/bin/gs"
            echo "Nix Shell environment is ready."
            echo "${pkgs.libGL}"
          '';
        };
      }
    );
}
  1. Run the example script provided in the MakieTeX documentation:
   using Makie, MakieTeX, CairoMakie

   fig = Figure()
   l1 = LTeX(
       fig[1, 1], L"A \emph{convex} function $f \in C$ is \textcolor{blue}{denoted} as \tikz{\draw[line width=1pt, >->] (0, -2pt) arc (-180:0:8pt);}";
       tellwidth = false, tellheight = true
   )
   ax1 = Axis(fig[2, 1])
   heatmap!(ax1, Makie.peaks())
   display(fig)
  1. Observe the error when the script tries to crop the PDF using Ghostscript:
 ❮onyr ★ nixos❯ ❮julia❯❯ nix develop 
warning: Git tree '/home/onyr/code/phd/julia' is dirty
Nix Shell environment is ready.
/nix/store/5mb70vg3kdzkyn0zqdgm4f87mdi0yi4i-libglvnd-1.7.0
 ❮onyr ★ nixos❯ ❮julia❯❯ julia examples/plots/test_latex.jl 
Ghostscript failed to get the bounding box of temp.pdf!
Files in temp directory are:
temp.aux,temp.fdb_latexmk,temp.fls,temp.log,temp.pdf,temp.tex
Stdout
Could not start dynamically linked executable: /home/onyr/.julia/artifacts/4883a6685d86b068da934537a444a6d170d71730/bin/gs
NixOS cannot run dynamically linked executables intended for generic
linux environments out of the box. For more information, see:
https://nix.dev/permalink/stub-ld

Stderr

ERROR: LoadError: 
Stacktrace:
  [1] error()
    @ Base ./error.jl:44
  [2] get_pdf_bbox(path::String)
    @ MakieTeX ~/.julia/packages/MakieTeX/y5p31/src/rendering/pdf_utils.jl:129
  [3] crop_pdf(path::String; margin::Vector{UInt8})
    @ MakieTeX ~/.julia/packages/MakieTeX/y5p31/src/rendering/pdf_utils.jl:152
  [4] crop_pdf
    @ ~/.julia/packages/MakieTeX/y5p31/src/rendering/pdf_utils.jl:146 [inlined]
  [5] (::MakieTeX.var"#56#58"{Cmd, Cmd, String, Cmd})()
    @ MakieTeX ~/.julia/packages/MakieTeX/y5p31/src/rendering/tex.jl:62
  [6] cd(f::MakieTeX.var"#56#58"{Cmd, Cmd, String, Cmd}, dir::String)
    @ Base.Filesystem ./file.jl:112
  [7] #55
    @ ~/.julia/packages/MakieTeX/y5p31/src/rendering/tex.jl:28 [inlined]
  [8] mktempdir(fn::MakieTeX.var"#55#57"{Cmd, Cmd, String, Cmd}, parent::String; prefix::String)
    @ Base.Filesystem ./file.jl:819
  [9] mktempdir (repeats 2 times)
    @ ./file.jl:815 [inlined]
 [10] #compile_latex#54
    @ ~/.julia/packages/MakieTeX/y5p31/src/rendering/tex.jl:27 [inlined]
 [11] compile_latex
    @ ~/.julia/packages/MakieTeX/y5p31/src/rendering/tex.jl:15 [inlined]
 [12] latex2pdf
    @ ~/.julia/packages/MakieTeX/y5p31/src/rendering/tex.jl:71 [inlined]
 [13] cached_doc(T::Type{CachedTEX}, f::typeof(MakieTeX.latex2pdf), doc::TEXDocument; kwargs::@Kwargs{})
    @ MakieTeX ~/.julia/packages/MakieTeX/y5p31/src/types.jl:526
 [14] cached_doc
    @ ~/.julia/packages/MakieTeX/y5p31/src/types.jl:525 [inlined]
 [15] CachedTEX
    @ ~/.julia/packages/MakieTeX/y5p31/src/types.jl:460 [inlined]
 [16] CachedTEX(x::LaTeXString; kwargs::@Kwargs{})
    @ MakieTeX ~/.julia/packages/MakieTeX/y5p31/src/types.jl:471
 [17] CachedTEX
    @ ~/.julia/packages/MakieTeX/y5p31/src/types.jl:466 [inlined]
 [18] _to_cachedtex
    @ ~/.julia/packages/MakieTeX/y5p31/src/layoutable.jl:38 [inlined]
 [19] call_composed
    @ ./operators.jl:1054 [inlined]
 [20] call_composed (repeats 2 times)
    @ ./operators.jl:1053 [inlined]
 [21] (::ComposedFunction{ComposedFunction{typeof(collect), typeof(tuple)}, typeof(MakieTeX._to_cachedtex)})(x::LaTeXString)
    @ Base ./operators.jl:1050
 [22] #map#13
    @ ~/.julia/packages/Observables/YdEbO/src/Observables.jl:570 [inlined]
 [23] map
    @ ~/.julia/packages/Observables/YdEbO/src/Observables.jl:568 [inlined]
 [24] initialize_block!(l::LTeX)
    @ MakieTeX ~/.julia/packages/MakieTeX/y5p31/src/layoutable.jl:49
 [25] _block(T::Type{LTeX}, fig_or_scene::Figure, args::Vector{Any}, kwdict::Dict{Symbol, Any}, bbox::Nothing; kwdict_complete::Bool)
    @ Makie ~/.julia/packages/Makie/Y3ABD/src/makielayout/blocks.jl:398
 [26] _block
    @ ~/.julia/packages/Makie/Y3ABD/src/makielayout/blocks.jl:317 [inlined]
 [27] #_block#1442
    @ ~/.julia/packages/Makie/Y3ABD/src/makielayout/blocks.jl:262 [inlined]
 [28] _block(::Type{LTeX}, ::GridPosition; kwargs::@Kwargs{tex::LaTeXString, tellwidth::Bool, tellheight::Bool})
    @ Makie ~/.julia/packages/Makie/Y3ABD/src/makielayout/blocks.jl:256
 [29] _block
    @ ~/.julia/packages/Makie/Y3ABD/src/makielayout/blocks.jl:249 [inlined]
 [30] #_#1440
    @ ~/.julia/packages/Makie/Y3ABD/src/makielayout/blocks.jl:240 [inlined]
 [31] Block
    @ ~/.julia/packages/Makie/Y3ABD/src/makielayout/blocks.jl:239 [inlined]
 [32] #LTeX#28
    @ ~/.julia/packages/MakieTeX/y5p31/src/layoutable.jl:36 [inlined]
 [33] top-level scope
    @ ~/code/phd/julia/examples/plots/test_latex.jl:5
in expression starting at /home/onyr/code/phd/julia/examples/plots/test_latex.jl:5

Error Message:

The following error is raised during execution:

Ghostscript failed to get the bounding box of temp.pdf!
Files in temp directory are:
temp.aux,temp.fdb_latexmk,temp.fls,temp.log,temp.pdf,temp.tex
Stdout

Stderr
Could not start dynamically linked executable: /home/onyr/.julia/artifacts/4883a6685d86b068da934537a444a6d170d71730/bin/gs
NixOS cannot run dynamically linked executables intended for generic
linux environments out of the box. For more information, see:
https://nix.dev/permalink/stub-ld

The culprit can be seen here where the code uses dynamically linked executable.


What I Have Tried:

  1. Setting a Custom Ghostscript Path:

    • I added the NixOS Ghostscript binary to the PATH and set it explicitly in the environment:
      export GHOSTSCRIPT_PATH=/nix/store/<path-to-ghostscript>/bin/gs
    • Updated the Julia script to use this path:
      using MakieTeX
      MakieTeX.Ghostscript_jll.gs() = ENV["GHOSTSCRIPT_PATH"]
    • However, the library still uses the bundled Ghostscript_jll.
  2. Verifying Ghostscript on NixOS:

    • Ran which gs to confirm the path:
       ❮onyr ★ nixos❯ ❮julia❯❯ which gs
      /nix/store/dasn4h9r40sp4rrvf7vp2aqnzzxba73s-ghostscript-with-X-10.04.0/bin/gs
      
    • Tested the binary:
      /nix/store/<path-to-ghostscript>/bin/gs -h
      The binary works as expected.
  3. Exploring Source Code:

    • Examined the get_pdf_bbox function in MakieTeX:
      Ghostscript_jll.gs()
      It seems to directly rely on the bundled Ghostscript_jll, which is incompatible with NixOS.

Environment:

  • Operating System: NixOS
  • Julia Version: 1.11.2 (2024-12-01)
  • MakieTeX.jl Version: v0.4.3

Expected Behavior:

The library should respect the custom Ghostscript path set via ENV["GHOSTSCRIPT_PATH"] or allow an explicit override for the Ghostscript executable, or even just check that the command gs is available on the current shell.


Suggested Fix:

  • Provide an option to completely bypass the bundled Ghostscript_jll and use a system-installed Ghostscript binary.
  • Allow users to configure the Ghostscript path in a straightforward manner, either through an environment variable or a function call.

Additional Context:

NixOS is a declarative Linux distribution where binaries are isolated, and dynamically linked executables intended for generic Linux environments may fail without additional configuration. The bundled Ghostscript_jll executable does not work on NixOS due to this reason.

Relevant link: https://nix.dev/permalink/stub-ld


Any help or advice appreciated.

@0nyr
Copy link
Author

0nyr commented Jan 7, 2025

So I created a PR to fix the issue. Now just including pkgs.ghostscript in your flake.nix and boom, it works !

However I'm fairly disappointed with the rendering. I get very pixelated results and no TikZ supports. It's strange since I have latexmk and lualatex available in my dev shell:

 ❮onyr ★ nixos❯ ❮julia❯❯ latexmk -v
Latexmk, John Collins, 7 Apr. 2024. Version 4.85
 ❮onyr ★ nixos❯ ❮julia❯❯ lualatex -v
This is LuaHBTeX, Version 1.18.0 (TeX Live 2024/nixos.org)
Development id: 7611

Execute  'luahbtex --credits'  for credits and version details.

There is NO warranty. Redistribution of this software is covered by
the terms of the GNU General Public License, version 2 or (at your option)
any later version. For more information about these matters, see the file
named COPYING and the LuaTeX source.

LuaTeX is Copyright 2022 Taco Hoekwater and the LuaTeX Team.

On the examples in the README, I get:

using Makie
include("../src/MakieTeX.jl")
using .MakieTeX
using CairoMakie # or whichever other backend

fig = Figure()
l1 = LTeX(
    fig[1, 1], L"A \emph{convex} function $f \in C$ is \textcolor{blue}{denoted} as \tikz{\draw[line width=1pt, >->] (0, -2pt) arc (-180:0:8pt);}";
    tellwidth = false, tellheight = true
)
ax1 = Axis(
    fig[2, 1];
)
heatmap!(ax1, Makie.peaks())

# Save the figure to a file in the current directory
save("output_image.png", fig)

display(fig)

Image

And:

using Makie
include("../src/MakieTeX.jl")
using .MakieTeX
using CairoMakie # or whichever other backend


fig = Figure(size = (400, 300));
tex1 = LTeX(
    fig[1, 1], 
    L"\int \mathbf E \cdot d\mathbf a = \frac{Q_{encl}}{4\pi\epsilon_0}", 
    scale=1,
);
tex2 = LTeX(
    fig[2, 1], 
    L"\int \mathbf E \cdot d\mathbf a = \frac{Q_{encl}}{4\pi\epsilon_0}", 
    scale=2,
    render_density = 5
);

# Save the figure to a file in the current directory
save("output_image2.png", fig)

Image

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

No branches or pull requests

1 participant