How do I package an app with a directory structure? (Java app)

Hi,

I’ve been working on packaging a Java application (Enonic XP) recently, and have made some progress, but I’m stuck now when working on creating an actual derivation. I’ll list my immediate questions first and then provide more context.

  • This application comes as an entire directory structure. How do I install that into the nix store?
  • There are other applications that expect to find this directory structure located at a specific point on disk (the user’s home folder). Is there a way to fake this? Do I create an automatic symlink? Do I have to configure the tools to look for the application in the Nix store somehow?
  • What is the ergonomic way of working with building Nix packages? How do I test whether the derivation works? It’s always seemed like a bit of magic to me.

I’ve also gotten some help with this process previously, both on this forum and on Enonic’s help forum, and I’m very grateful for that. This feels like a new step and a new question, however, so I feel like a new thread is appropriate.

Context

I recently started working with a Java-based CMS called Enonic XP. When you install a version of it, it installs a tree structure into your home folder that includes, among other things, a Java executable and some shell scripts that start everything. As expected, this doesn’t run well on NixOS, because the Java executable isn’t patched. More details on how this manifests can be found in the Enonic forum thread.

After finding other workarounds, I finally decided I’d try and patch it properly. I followed the tips given in this well-cited Stack Exchange answer and managed to manually patch the Java executable (more details available in this post from the Enonic forum thread).

Next would be packaging it so that it would be easy to re-use and share. Following the same Stack Exchange post and the NixOS wiki document on packaging/binaries, I’ve managed to create a derivation that seems to almost get it right, but I’m stuck at what feels like the end: the installation phase. I’m also not sure exactly how I’d use this.

Derivation

Here’s the derivation as it stands currently:

{ alsaLib
, autoPatchelfHook
, fetchurl
, freetype
, glibc
, lib
, libX11
, libXext
, libXi
, libXtst
, stdenv
, unzip
, xorg
, zlib
}:

let
  version = "7.6.1";

  url =
    "https://repo.enonic.com/public/com/enonic/xp/enonic-xp-linux-sdk/${version}/enonic-xp-linux-sdk-7.6.1.zip";

in
stdenv.mkDerivation {
  name = "enonic-xp-${version}";
  inherit version;

  src = fetchurl {
    sha256 = "0c58zcyddxa0041bvyafyz8250ylcnqgh4ckqgyq078dzzkg5mbd";
    inherit url;
  };

  nativeBuildInputs = [ autoPatchelfHook unzip ];

  buildInputs = [
    alsaLib
    freetype
    glibc
    libX11
    libXext
    libXi
    libXtst
    xorg.libXrender
    zlib
  ];

  unpackPhase = ''
    unzip $src
  '';

  installPhase = ''
    mkdir -p $out
    # install -m755 -D enonic-xp-linux-sdk-${version}/* -t $out
    cp -r enonic-xp-linux-sdk-${version}/* $out/
  '';

  meta = with lib; {
    description = "Enonic XP distribution";
    homepage = "https://enonic.com";
    license = licenses.gpl3;
    maintainers = with stdenv.lib.maintainers; [];
    platforms = platforms.linux;
  };

}

It seems to be able to patch the elf correctly (though I needed a lot more dependencies than when I did the manual patch). After the patching, it seems as if it’s trying to unpack and build something again. The build log ends with

setting RPATH to: /nix/store/rldppqna2kya26zpdrl7p1wlbz0jgvj3-zlib-1.2.11/lib:/nix/store/7j0yfkjx2fc4hq0qswgm8nr2dwblz933-enonic-xp-7.6.1/jdk/lib/jli
building '/nix/store/s3j0lxph6rc43qgdd8yn8m87329ldjdn-enonic-xp.drv'...
unpacking sources
unpacking source archive /nix/store/7j0yfkjx2fc4hq0qswgm8nr2dwblz933-enonic-xp-7.6.1
source root is enonic-xp-7.6.1
patching sources
configuring
no configure script, doing nothing
building
no Makefile, doing nothing
installing
install flags: SHELL=/nix/store/9ywr69qi622lrmx5nn88gk8jpmihy0dz-bash-4.4-p23/bin/bash install
make: *** No rule to make target 'install'.  Stop.
builder for '/nix/store/s3j0lxph6rc43qgdd8yn8m87329ldjdn-enonic-xp.drv' failed with exit code 2

For building I use nix-build and a default.nix right next to the above derivation which looks like this:

{ pkgs ? import <nixpkgs> {}, ... }:

with pkgs;
stdenv.mkDerivation {
  name = "enonic-xp";
  src = pkgs.callPackage (import ./enonic-xp.nix) {};
}

Upon closer inspection, I now realize that this is probably why the build fails: it tries to build the output of the callPackage import? (Aha moment!)

Questions

Now that we’ve got some more context, let me try and elaborate on the questions a bit.

Installing a directory structure

When unzipping the sources in the derivation’s install phase, I end up with a directory tree. I’d like all the contents of this tree to be available in the Nix store. I tried using install with a number of different arguments, but ended up falling back to cp -r which seems to work (inspired by this Stack Overflow question).

Is this the ‘right’/recommended way of doing it? Is there a way to make it work with install or is this just as good?

Making the application available to other apps

Some other applications expect to find this application (the whole directory tree) at a specified point on disk (in a directory in the user’s home directory). How should I go about doing this? Can I create a symlink from the expected location to the nix store? I think home-manager does something like that, but I don’t know if it’s recommended or how I’d do it. If it is the right way to do it, how do I do it? And if it isn’t, what options are there?

How do I work with building nix packages

For all the reading I’ve done (or at least tried to do) on building packages with Nix, I haven’t been able to find much that explains concisely how I build packages and what I need. What’s the build-test feedback loop?

For instance, the reason I created a default.nix to import this package and build it, is that just using nix-build on the package.nix file gives an error saying you ‘cannot auto-call a function that has an argument without a default value’. I understand that this is because the package file is just a function, but I also thought you shouldn’t manually import <nixpkgs> into a package. So then: how do I build it and how do I test it?

Is the nix repl relevant here? If so, how do I use that? I still don’t understand what I should use the repl for (but I guess that’s for a different thread).


Apologies for the long question, but any input (towards any of the questions or just tips in general) would be very much appreciated! :pray: I’m also happy to supply any other information you may want; just let me know and I’ll try to get back to you as soon as possible. Also: if I’m going about this in the wrong way, that’s also useful info!

Cheers!

Installing a directory structure

cp -r is fine as long as you don’t care about symbolic links and maybe xattr. If you want to preserve as much as possible go with cp -a

Making the application available to other apps

A symbolic link from the expected location to the Nix store is pretty much the standard way of doing it. If you are using home-manager the options under home.file is where I would declare such links.

How do I work with building nix packages

When developing nix packages I will usually have the derivation declared in one file like you have done with ./enonic-xp.nix and then have a package.nix file that I can build with nix-build or run with nix-shell for debugging the derivation. My package.nix will usually look something like this:

{ nixpkgs ? <nixpkgs>
, pkgs ? import nixpkgs {} }:
pkgs.callPackage ./enonic-xp.nix {};

callPackage takes a path and an attrset with argument overrides and returns a derivation so you don’t have to make a second derivation like you have done in your default.nix.

1 Like

Oh, sweet; Thank you! That package.nix tip is definitely simplifies things :smile:

On making the application available to other apps:
I do use home-manager, but wouldn’t it be better if this wasn’t dependent on home-manager? At least I thought this would ideally just be installable from wherever as long as you’ve got Nix.

And a new question if you don’t mind: is this the right way to go about this? Context: I managed to get the packaging to work and all the files do end up correctly in the nix store and the patching works as expected. However, when starting the application, it starts throwing error messages and fails to start because it’s trying to write logs and create cache directories in the Nix store (which is famously a read-only file system).

I hadn’t really thought much about that at all, but it makes me think that maybe I’m going about this all wrong. Patching the Elf and the shebang lines is definitely necessary for it to work properly, but I’m not sure how best to package this for Nix if it doesn’t go in the store.

Hmm, surely there must be other applications that do this, though :thinking: How do they handle it? Maybe place cache and logs in user land (if the app can be configured that way)?

Do you have any tips?

Sorry for just launching in with more questions; I hope you don’t mind too much. I really appreciate your help!

sure - the ideal solution would be to allow the other apps to find your app even if it’s not in your home directory, for example by scanning the PATH or setting another environment variable…

Indeed I think the ideal solution would be to install the application itself to the nix store, but have it write logs and create cache directories elsewhere… ~/.cache for the cache maybe. ideally to journalctl for the logs, but that might be nontrivial, so perhaps some other directory in ~ or /tmp or similar?

I did a little research on Enomic XP and it seems to support an env variable XP_HOME or java property xp.home that is used to find the location where it writes things to. Normally this is set to home/ of the SDK installation location but you can change that.

1 Like

@griff Oh, wow, yeah, you’re right! Setting the XP_HOME variable manually before trying to start the application actually works. Is there a way to set env variables in packages? That might work around that.

Hey! Nice to hear from you again and thanks! I’m not sure I’m able to specify different directories for logs and caches (but I’ll certainly try and find out). But I reckon the first thing is to just write them to the same place using XP_HOME as @griff correctly pointed out.

Hey! It’s been a while, but I’ve finally gotten around to working some more on this. I’ve got some more discoveries and some new questions. Let me sum up briefly:

  • As mentioned in the last few posts, I did manage to package the XP distribution successfully.
  • When setting the XP_HOME env var, it uses that variable.

However, the distro shouldn’t really be interacted with directly. Instead, the user should use the Enonic CLI. The CLI sets the XP_HOME variable ~/.enonic, which is how it works with multiple distributions and sandboxes (instances).

Now, if I symlink ~/.enonic/distributions/<distro> to the corresponding distro in the Nix store, the CLI (a pre-release build that follows symlinks) and the actual distro seem to work perfectly together (:partying_face:).

This is where my questions start:

Can a package create symlinks in the user’s environment? I know Home Manager has a way of doing this, but is it possible outside of using Home Manager? As mentioned above, I’d prefer if this didn’t depend on HM. From what I understand, this isn’t (and probably shouldn’t be** possible.

Solutions / workarounds?

If creating an automatic symlink doesn’t work, these are the alternatives I can think of. Do any of these sound viable? Is there a better way around this?

Create manual symlinks. The user can always create their own symlinks, but with the hashes in the Nix store and the extra manual labor, this isn’t exactly very user friendly.

Bundle the distro and CLI together and couple them tightly. After going through the source code, I found that the CLI uses the HOME env to determine where distros are found. In theory, it might be possible to write the distro to $out/.enonic/distributions and then set HOME to $out in a wrapper script for the CLI. However, this would also mean that
1. The CLI would only work with one distro at a time
2. The CLI would try to write files to $out/.enonic/sandboxes, which would probably error out.

Create a /usr/.enonic dir to write/link the distro to. If it’s possible to write/link to constant (not dependent on the user) directory, then Nix could create files here. If user also has write permissions here, then the CLI could use this as HOME. However, setting a different `HOME** feels like it might have unintended consequences down the road.

CLI feature request: allow --xp-distro-dir config option. Either via an env var or via an option. The CLI would then use this directory instead of (or in addition to) the standard ~/.enonic/distributions.

CLI feature request: add global distro directory. Add a global distro directory (e.g. /etc/enonic/distributions) that the CLI uses to look for distros in addition to ~/.enonic/distributions OR that is used as a fallback if the latter does not exist.

The last two options would also require the dev team to accept these changes as something they’d want in the CLI and then to work on them (or accept a PR from someone else who does). However, in the absence of symlinking in the users home directory, the last option (global distro dir) sounds like the most appropriate one for me. Though with the disclaimer that I do not know what the common directory structure or accepted directories to put stuff in is (is it /etc? /usr? I really don’t know :person_shrugging:)

Workflow / goal

Which solution is the most appropriate is probably dependent on how the workflow would look like. Here’s what I imagine.

  • The user should be able to use a Nix shell to get hold of both XP and the CLI.
  • Both should also be possible to install globally or via Home Manager.

However, I’m not sure whether the distros available to the CLI should be restricted to what’s available via the current packages. I don’t know how to explain it, so let’s try this example. Given the following structure:

projects
├─ project-a
└─ project-b

If both projects a and b have an shell.nix files with different versions of the XP distro (say, a has 7.6.0 and b has 7.6.1), should running the CLI (available either globally or from the same shell.nix) from project-a also make 7.6.1 (b’s version) available?

Thinking about this, it does seem to go against Nix’s general idea of isolation (even if only by accident). That’s a point against the global distro store. I’m not really sure what to make of this.


Any input would be much appreciated! And if this is too much of a departure from the discussion thus far, it might be better to start a new thread for it, but it depends on a lot of context from this one, so I’m putting it here first :smiling_face: