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

Julia build system #208379

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d035d11
julia: Add Julia package build system as 'withPackages' passthru
fedeinthemix Dec 23, 2022
0f6a870
julia2nix: Init at 20221231
fedeinthemix Dec 30, 2022
f460f6e
julia: Add a NixOS release-note entry about the addition of withPacka…
fedeinthemix Dec 31, 2022
1855c80
julia: use 'isELF' and 'stdenv.cc.bintools.dynamicLinker' in build-ju…
fedeinthemix Dec 31, 2022
c88332d
julia: Document use of withPackages and juliaPackages attributes
fedeinthemix Dec 31, 2022
98dc845
julia2nix.jl: Add pkg_slug function
fedeinthemix Jan 1, 2023
014bcbb
julia2nix.jl: Allow to specify package version with 'Pkg' syntax
fedeinthemix Jan 1, 2023
5053486
julia2nix.jl: Add option to use Project.toml file
fedeinthemix Jan 1, 2023
83ba3f8
julia2nix.jl: Add short varsion of options and improve error handling
fedeinthemix Jan 2, 2023
36c2e7a
julia: Refine build system
fedeinthemix Jan 2, 2023
d6e4c7b
julia2nix.jl: include lazy artifacts
fedeinthemix Jan 2, 2023
9d82df7
julia: Add julia to passthru attributes
fedeinthemix Jan 3, 2023
8854488
julia: update buildPhase to check for 'deps/build.jl` and add use of …
fedeinthemix Jan 3, 2023
c47be78
julia: Make Julia's fontonfig find system files
fedeinthemix Jan 6, 2023
92ea844
julia2nix: Add options to generate 'flake.nix' and 'shell.nix'
fedeinthemix Jan 9, 2023
4140d82
julia: Add configurePhase
fedeinthemix Jan 10, 2023
8918dcc
julia: Separate nixpkgs julia package collection from upstream one to…
fedeinthemix Jan 11, 2023
2fbc9d8
julia: Make 'Pkg' prefer local packages by setting it "offline"
fedeinthemix Jan 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/languages-frameworks/index.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<xi:include href="java.section.xml" />
<xi:include href="javascript.section.xml" />
<xi:include href="lua.section.xml" />
<xi:include href="julia.section.xml" />
<xi:include href="maven.section.xml" />
<xi:include href="nim.section.xml" />
<xi:include href="ocaml.section.xml" />
Expand Down
72 changes: 72 additions & 0 deletions doc/languages-frameworks/julia.section.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Julia {#sec-julia}

## Introduction {#ssec-julia-introduction}

The [Julia programming language](https://julialang.org/) was developed
with scientific applications in mind. It is distributed with a package
manager and the vast majority of packages are available through the
main package server
[pkg.julialang.org](https://pkg.julialang.org). While the native
package manager can be used to easily and declaratively install
packages, several ones come with binary blobs (called artifacts in
Julia parlance) that do not work in NixOS. To remedy this shortcoming,
Julia packages can be installed as described below. This way the
binary blobs are patched to work as expected and all software can be
declared in a single file.

### Installing Julia Packages {#sssec-installing-julia-packages}

Most Julia compilers available in Nixpkgs have a passthru attribute
called `withPackages`. This function permits creating Julia
environments with sets of packages ready for use. Compared with the
similarly named function available in other Nixpkgs language
frameworks, the one of Julia takes two arguments. The first one is a
function specifying a list of Julia packages available in Nixpkgs. The
second one is also a function, and it's used to specify a list of
extra package definitions not available in Nixpkgs. These definitions
can be generated with the help of `julia2nix`. As an example, to
install `julia-bin` with the Nixpkgs Julia package `Plots`, all its
dependencies and the upstream package `CSTParser` with its dependency
`Tokenize`, one can use the following command

```ShellSession
$ nix-shell -p 'julia-bin.withPackages
(p: with p; [ Plots ])
(p: with p; [
{ pname = "Tokenize";
version = "0.5.25";
src = fetchurl {
url = "https://pkg.julialang.org/package/0796e94c-ce3b-5d07-9a54-7f471281c624/90538bf898832b6ebd900fa40f223e695970e3a5";
name = "julia-bin-1.8.3-Tokenize-0.5.25.tar.gz";
sha256 = "00437718f09d81958e86c2131f3f8b63e0975494e505f6c7b62f872a5a102f51";
};
}

{ pname = "CSTParser";
version = "3.3.6";
src = fetchurl {
url = "https://pkg.julialang.org/package/00ebfdb7-1f24-5e51-bd34-a7502290713f/3ddd48d200eb8ddf9cb3e0189fc059fd49b97c1f";
name = "julia-bin-1.8.3-CSTParser-3.3.6.tar.gz";
sha256 = "647fc5588cb87362d216401a0a4124d41b80a85618a94098a737ad38ae5786c4";
};
requiredJuliaPackages = [ Tokenize ];
}
])
'
```

This is not very practical as a `nix-shell` command, but the same
expression can be used in a `shell.nix` file, or other places
expecting a Nix expression as described elsewhere in this
manual. After executing this command and starting Julia, the packages
can be directly imported with `using` or `import` without first having
to use `import Pkg; Pkg.add(...)`.

The list of Julia packages in Nixpkgs is accessible through the
compiler passthru attribute set `juliaPackages`, for example
`julia-bin.juliaPackages`.

Note that the above upstream package definitions are addressed with an
`url` specifying a unique package identifier (uuid) and the hash of
the package content. For this reason we don't lose reproducibility by
using upstream packages.
13 changes: 12 additions & 1 deletion nixos/doc/manual/from_md/release-notes/rl-2305.section.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,25 @@
In addition to numerous new and upgraded packages, this release
has the following highlights:
</para>
<itemizedlist spacing="compact">
<itemizedlist>
<listitem>
<para>
Cinnamon has been updated to 5.6, see
<link xlink:href="https://github.com/NixOS/nixpkgs/pull/201328#issue-1449910204">the
pull request</link> for what is changed.
</para>
</listitem>
<listitem>
<para>
It’s now possible to make
<link xlink:href="https://julialang.org">Julia</link>
environments with a set of packages using e.g.
<literal>julia-bin.withPackages</literal>. Definitions for
packages in the main Julia package repository not available in
NixOS can be generated with <literal>julia2nix</literal> and
used directly with <literal>withPackages</literal>.
</para>
</listitem>
</itemizedlist>
</section>
<section xml:id="sec-release-23.05-new-services">
Expand Down
2 changes: 2 additions & 0 deletions nixos/doc/manual/release-notes/rl-2305.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ In addition to numerous new and upgraded packages, this release has the followin

- Cinnamon has been updated to 5.6, see [the pull request](https://github.com/NixOS/nixpkgs/pull/201328#issue-1449910204) for what is changed.

- It's now possible to make [Julia](https://julialang.org) environments with a set of packages using e.g. `julia-bin.withPackages`. Definitions for packages in the main Julia package repository not available in NixOS can be generated with `julia2nix` and used directly with `withPackages`.

## New Services {#sec-release-23.05-new-services}

<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
Expand Down
24 changes: 24 additions & 0 deletions pkgs/development/compilers/julia/build-env.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{ buildEnv
, makeWrapper
, extraPackages ? []
, julia
, computeRequiredJuliaPackages
}:

buildEnv {
name = "julia-env";
paths = computeRequiredJuliaPackages extraPackages;

nativeBuildInputs = [ makeWrapper ];

pathsToLink = ["/share/julia" ];

postBuild = ''
makeWrapper ${julia}/bin/julia $out/bin/julia \
--set JULIA_LOAD_PATH "$JULIA_LOAD_PATH:$out/share/julia/packages" \
--set JULIA_DEPOT_PATH "$JULIA_DEPOT_PATH:$out/share/julia"
'';

passthru = { inherit julia; };
}

161 changes: 161 additions & 0 deletions pkgs/development/compilers/julia/build-julia-package.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
{ lib
, stdenv
, config
, patchelf
, julia
, computeRequiredJuliaPackages
, computeJuliaDepotPath
, computeJuliaLoadPath
, computeJuliaArtifacts
}:

# Build an individual package
{ fullPkgName ? "${attrs.pname}-${attrs.version}"

, src

, dontPatch ? false
, patches ? []
, patchPhase ? ""

, enableParallelBuilding ? true
# Build-time dependencies for the package, which were compiled for the system compiling this.
, nativeBuildInputs ? []

# Build-time dependencies for the package, which may not have been compiled for the system compiling this.
, buildInputs ? []

# Propagate build dependencies so in case we have A -> B -> C,
# C can import package A propagated by B
# Run-time dependencies for the package.
, propagatedBuildInputs ? []

# Julia packages that are required at runtime for this one.
# These behave similarly to propagatedBuildInputs, where if
# package A is needed by B, and C needs B, then C also requires A.
# The main difference between these and propagatedBuildInputs is
# during the package's installation into Julia, where all
# requiredJuliaPackages are ALSO installed into Julia.
, requiredJuliaPackages ? []

, preBuild ? ""
, meta ? {}
, passthru ? {}

# Julia packages can distribute 'Artifacts'. These can be some data,
# binary, ... We Package 'Artifacts' as independent derivations. They
# differ from the rest of the packages in that they don't get compiled
# and the package providing it expect to find them in a specific location.
, isJuliaArtifact ? false
, ... } @ attrs:

let
requiredJuliaPackages' = computeRequiredJuliaPackages requiredJuliaPackages;
juliaDepotPath = computeJuliaDepotPath requiredJuliaPackages';
juliaLoadPath = computeJuliaLoadPath requiredJuliaPackages';
juliaArtifacts = computeJuliaArtifacts requiredJuliaPackages';


# Must use attrs.nativeBuildInputs before they are removed by the removeAttrs
# below, or everything fails.
nativeBuildInputs' = [ julia patchelf ] ++ nativeBuildInputs;

# This step is required because when
# a = { test = [ "a" "b" ]; }; b = { test = [ "c" "d" ]; };
# (a // b).test = [ "c" "d" ];
# This used to mean that if a package defined extra nativeBuildInputs, it
# would override the ones for building a Julia package (Julia itself)
# causing everything to fail.
attrs' = builtins.removeAttrs attrs [ "nativeBuildInputs" ];

in stdenv.mkDerivation ({
packageName = "${fullPkgName}";
# The name of the package ends up being
# "julia-version-package-version"
name = "${julia.pname}-${julia.version}-${fullPkgName}";

# This states that any package built with the function that this returns
# will be a julia package. This is used for ensuring other julia
# packages are installed into julia during the environment building phase.
isJuliaPackage = true;
# Julia Artifacts need to be treated specially.
isJuliaArtifact = isJuliaArtifact;

inherit src;

inherit dontPatch patches patchPhase;

dontConfigure = true;

enableParallelBuilding = enableParallelBuilding;

requiredJuliaPackages = requiredJuliaPackages';

nativeBuildInputs = nativeBuildInputs';

buildInputs = buildInputs ++ requiredJuliaPackages' ++ [ stdenv.cc.libc ];

propagatedBuildInputs = propagatedBuildInputs;

preUnpack = ''
mkdir ${attrs.pname}
cd ${attrs.pname}
'';

setSourceRoot = ''
sourceRoot=$PWD
'';

# Julia installs compiled cache in the first path in DEPOT_PATH.
# Packages are pre-complied when imported.
buildPhase = if isJuliaArtifact then ''
runHook preBuild

mkdir -p $out/share/julia/artifacts/${attrs.juliaPath}
cp -r * $out/share/julia/artifacts/${attrs.juliaPath}

runHook postBuild
'' else ''
runHook preBuild

mkdir -p $out/share/julia/packages/${attrs.pname}
cp -r * $out/share/julia/packages/${attrs.pname}

export JULIA_LOAD_PATH=$out/share/julia/packages:${juliaLoadPath}:$JULIA_LOAD_PATH
export JULIA_DEPOT_PATH=$out/share/julia:${julia}/share/julia:${juliaDepotPath}

mkdir -p $out/share/julia/artifacts
for path in ${lib.concatMapStringsSep " " (p: p + "/share/julia/artifacts/*") juliaArtifacts}; do
ln -s $path $out/share/julia/artifacts/$(basename $path);
done

pushd $out/share/julia/packages/${attrs.pname}
if [[ -f ./deps/build.jl ]]; then
pushd deps
$NIX_JULIA_PRECMD julia -- build.jl
popd
fi
$NIX_JULIA_PRECMD julia -e 'import ${attrs.pname}'
popd

# these may cause collisions
rm -r $out/share/julia/logs || true
rm -r $out/share/julia/scratchspaces || true

runHook postBuild
'';

# Need to install before building
dontInstall = true;

# Patch interpreter of bundled binary files
postFixup = if isJuliaArtifact then ''
if [[ -d "$out"/share/julia/artifacts/${attrs.juliaPath}/bin ]]; then
for fn in $(echo $out/share/julia/artifacts/${attrs.juliaPath}/bin/*); do
isELF $fn && patchelf --set-interpreter ${stdenv.cc.bintools.dynamicLinker} $fn || true
done;
fi
'' else "";

inherit meta;
} // attrs')
68 changes: 68 additions & 0 deletions pkgs/development/compilers/julia/with-packages.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{ lib, callPackage, julia, fetchurl }:

# The function `withPackages` can be used to create a Julia environment
# with a specified set of packages as shown by the following example
#
# pkgs.julia-bin.withPackages
# (p: with p; [ Plots ])
# (p: with p; [
# { pname = "Tokenize";
# version = "0.5.25";
# src = fetchurl {
# url = "https://pkg.julialang.org/package/0796e94c-ce3b-5d07-9a54-7f471281c624/90538bf898832b6ebd900fa40f223e695970e3a5";
# name = "julia-bin-1.8.3-Tokenize-0.5.25.tar.gz";
# sha256 = "00437718f09d81958e86c2131f3f8b63e0975494e505f6c7b62f872a5a102f51";
# };
# }
#
# { pname = "CSTParser";
# version = "3.3.6";
# src = fetchurl {
# url = "https://pkg.julialang.org/package/00ebfdb7-1f24-5e51-bd34-a7502290713f/3ddd48d200eb8ddf9cb3e0189fc059fd49b97c1f";
# name = "julia-bin-1.8.3-CSTParser-3.3.6.tar.gz";
# sha256 = "647fc5588cb87362d216401a0a4124d41b80a85618a94098a737ad38ae5786c4";
# };
# requiredJuliaPackages = [ Tokenize ];
# }
# ])
#
# The first argument specifies a list of packages available in `<nixpkgs>`. The second one
# a list of upstream packages specified by a package definition. These definitions can be
# automatically generated by `julia2nix`. For all of them the package attribute name
# is equal to the `pname` of the package.
#
# The reason for using the attribute `requiredJuliaPackages` rather than
# `propagatedBuildInputs` is that the dependencies must be installed as well.
#
# Packages artifacts are specified in a similar way with the addition of the
# `isArtifact` attribute. This is so because they need to be treated differently.

let juliaPackages = lib.recurseIntoAttrs
(callPackage ../../../top-level/julia-packages.nix { inherit julia; }).pkgs;

withPackages = nix: upstream:
let nixPackages = (nix juliaPackages);
upstreamPackages = (packagesFromUpstream upstreamPkgsList);

packagesFromUpstream = ps: builtins.map (p: upstreamPkgs."${p.pname}") ps;
Copy link
Contributor

@benneti benneti Jan 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to handle this using two arguments, instead of checking whether the package is an element of the definitions in nix and if not call juliaPackages.juliaPackagesFromList or export juliaPackages.juliaPackagesFromList and make it more similar to python where it is possible to call python3.withPackages (p: [ (python3Packages.buildPythonPackage) ]
such that the function call would look more like
julia.withPackages (p: [ p.Plots ] ++ (juliaPackages.juliaPackagesFromList (import ./packagelist.nix ...))).
But someone more experienced with nix package standards might be better to judge this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to use a single argument first and hit an infinite recursion problem. The packages refer to each other before they are defined. The trick to make it work in a lazy language is to build a fix point, a widely exploited trick in nix. However, if you try to process them, then the evaluation doesn't stop. My solution was to separate the lists so as not having to use them before they get defined. It may be possible to use a single argument, but at the moment I don't know how to do it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have an idea either, maybe someone more experienced like @SuperSandro2000 or @bennofs could give an opinion.


upstreamPkgsList = upstream upstreamPkgs;

upstreamPkgs = juliaPackages.juliaPackagesFromList upstreamPkgsList;

in callPackage ./build-env.nix {
inherit julia;
extraPackages = lib.unique nixPackages ++ upstreamPackages;
computeRequiredJuliaPackages = juliaPackages.computeRequiredJuliaPackages;
};

juliaWithPkgs = julia.overrideAttrs (finalAttrs: previousAttrs:
{ passthru = let passthru' = { juliaPackages = juliaPackages;
withPackages = withPackages;
};
in if previousAttrs ? passthru
then previousAttrs.passthru // passthru'
else passthru';
});

in juliaWithPkgs
Loading