I use chezmoi to manage my dotfiles, including a few small scripts that install development tools in my home directory.
One of these scripts installs Apache Maven. I keep it as a run_onchange_after_ script so that chezmoi runs it only when the script changes. This is especially convenient for versioned tools: when I want to upgrade Maven, I change the version number in the script, and the next chezmoi apply installs the new version.
The script is called:
|
1 2 |
run_onchange_after_20-install-maven.sh |
Here is the complete script.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
#!/usr/bin/env sh set -eu # Change this value to make chezmoi run this script again. MAVEN_VERSION="3.9.16" MAVEN_MAJOR="${MAVEN_VERSION%%.*}" INSTALL_ROOT="${HOME}/.local/opt" BIN_ROOT="${HOME}/.local/bin" ARCHIVE_NAME="apache-maven-${MAVEN_VERSION}-bin.tar.gz" DIST_PATH="maven/maven-${MAVEN_MAJOR}/${MAVEN_VERSION}/binaries" DOWNLOAD_URL="https://dlcdn.apache.org/${DIST_PATH}/${ARCHIVE_NAME}" CHECKSUM_URL="https://downloads.apache.org/${DIST_PATH}/${ARCHIVE_NAME}.sha512" # Fallback for older hardcoded versions after they leave the main mirrors. ARCHIVE_DOWNLOAD_URL="https://archive.apache.org/dist/${DIST_PATH}/${ARCHIVE_NAME}" ARCHIVE_CHECKSUM_URL="https://archive.apache.org/dist/${DIST_PATH}/${ARCHIVE_NAME}.sha512" TARGET_DIR="${INSTALL_ROOT}/apache-maven-${MAVEN_VERSION}" CURRENT_LINK="${INSTALL_ROOT}/maven" log() { printf '%s\n' "$*" } die() { printf 'ERROR: %s\n' "$*" >&2 exit 1 } download_file() { url="$1" output="$2" if command -v curl >/dev/null 2>&1; then curl -fL --retry 3 --connect-timeout 20 -o "$output" "$url" elif command -v wget >/dev/null 2>&1; then wget -q --tries=3 -O "$output" "$url" else die "Neither curl nor wget is installed." fi } download_file_with_fallback() { primary_url="$1" fallback_url="$2" output="$3" if download_file "$primary_url" "$output"; then return 0 fi log "Primary Apache mirror failed; trying Apache archive..." download_file "$fallback_url" "$output" } extract_sha512_from_file() { checksum_file="$1" awk ' { for (i = 1; i <= NF; i++) { token = $i gsub(/[^0-9a-fA-F]/, "", token) if (length(token) == 128) { print token exit } } } ' "$checksum_file" } sha512_of_file() { file="$1" if command -v sha512sum >/dev/null 2>&1; then sha512sum "$file" | awk '{ print $1 }' elif command -v shasum >/dev/null 2>&1; then shasum -a 512 "$file" | awk '{ print $1 }' elif command -v openssl >/dev/null 2>&1; then openssl dgst -sha512 "$file" | awk '{ print $NF }' else die "No SHA-512 tool found. Install sha512sum, shasum, or openssl." fi } create_bin_link() { name="$1" src="${CURRENT_LINK}/bin/${name}" dst="${BIN_ROOT}/${name}" if [ ! -e "$src" ]; then return 0 fi if [ -L "$dst" ] || [ ! -e "$dst" ]; then ln -sfn "$src" "$dst" else log "Keeping existing non-symlink: $dst" fi } mkdir -p "$INSTALL_ROOT" "$BIN_ROOT" if [ -x "${TARGET_DIR}/bin/mvn" ]; then log "Maven ${MAVEN_VERSION} is already installed at ${TARGET_DIR}." else tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/install-maven.XXXXXX")" trap 'rm -rf "$tmpdir"' EXIT INT TERM archive_file="${tmpdir}/${ARCHIVE_NAME}" checksum_file="${tmpdir}/${ARCHIVE_NAME}.sha512" log "Downloading Maven ${MAVEN_VERSION}..." download_file_with_fallback "$DOWNLOAD_URL" "$ARCHIVE_DOWNLOAD_URL" "$archive_file" log "Downloading SHA-512 checksum..." download_file_with_fallback "$CHECKSUM_URL" "$ARCHIVE_CHECKSUM_URL" "$checksum_file" expected_sha512="$(extract_sha512_from_file "$checksum_file")" [ -n "$expected_sha512" ] || die "Could not parse SHA-512 checksum file." actual_sha512="$(sha512_of_file "$archive_file")" expected_sha512="$(printf '%s' "$expected_sha512" | tr 'A-F' 'a-f')" actual_sha512="$(printf '%s' "$actual_sha512" | tr 'A-F' 'a-f')" [ "$expected_sha512" = "$actual_sha512" ] || die "SHA-512 checksum mismatch." log "Extracting Maven ${MAVEN_VERSION}..." tar -xzf "$archive_file" -C "$tmpdir" extracted_dir="${tmpdir}/apache-maven-${MAVEN_VERSION}" [ -x "${extracted_dir}/bin/mvn" ] || die "Extracted Maven archive is invalid." rm -rf "$TARGET_DIR" mv "$extracted_dir" "$TARGET_DIR" log "Installed Maven ${MAVEN_VERSION} at ${TARGET_DIR}." fi if [ -L "$CURRENT_LINK" ] || [ ! -e "$CURRENT_LINK" ]; then ln -sfn "$TARGET_DIR" "$CURRENT_LINK" else die "${CURRENT_LINK} exists and is not a symlink." fi create_bin_link mvn create_bin_link mvnDebug log "Maven ${MAVEN_VERSION} is available at: ${CURRENT_LINK}/bin/mvn" case ":${PATH:-}:" in *":${BIN_ROOT}:"*) ;; *) log "Add this to your shell config if needed:" log " export PATH=\"${BIN_ROOT}:\$PATH\"" ;; esac |
What this script does
The script installs a specific Maven version into my home directory, without requiring root privileges and without relying on a distribution package manager.
The resulting layout is:
|
1 2 3 4 5 6 7 |
~/.local/opt/ ├── apache-maven-3.9.16/ └── maven -> apache-maven-3.9.16 ~/.local/bin/ ├── mvn -> ~/.local/opt/maven/bin/mvn └── mvnDebug -> ~/.local/opt/maven/bin/mvnDebug<code class="language-text"></code> |
The versioned installation lives under ~/.local/opt/apache-maven-<version>, while ~/.local/opt/maven is a stable symbolic link to the currently selected Maven version.
This gives me two useful properties at the same time: I can keep explicit versioned installations, but I can also refer to Maven through a stable path.
Why run_onchange_after_
chezmoi treats scripts with names such as run_onchange_after_20-install-maven.sh specially.
The run_onchange_ part means the script is executed when its contents change. The after part means it runs after chezmoi has applied the rest of the dotfiles.
That is exactly what I want here. Maven does not need to be downloaded on every chezmoi apply. It only needs to be installed when I change something relevant in the script, such as the Maven version.
To upgrade Maven, I change this line:
|
1 2 |
MAVEN_VERSION="3.9.16" |
Then I run:
|
1 2 |
chezmoi apply |
Since the script content changed, chezmoi runs it again and installs the new Maven version.
Why install under ~/.local
I prefer installing this kind of tool under my home directory:
|
1 2 |
~/.local/opt |
and exposing commands through:
|
1 2 |
~/.local/bin |
This avoids using sudo, keeps the installation independent from the operating system package manager, and makes the setup easier to reproduce across machines.
It also avoids interfering with any Maven version installed system-wide. The Maven installed by this script is simply the one that appears first in my PATH.
Downloading from Apache mirrors
The script constructs the Maven download URL based on the configured version. For current Maven releases, it downloads from the main Apache download infrastructure.
There is also a fallback to the Apache archive. This matters because older versions are eventually removed from the main mirrors. Since dotfiles often pin specific versions for a long time, falling back to the archive makes the script more robust.
This is useful when setting up a new machine months later: even if the pinned Maven version is no longer on the primary mirror, the script can still find it in the Apache archive.
Verifying the archive
The script downloads both the Maven archive and the corresponding SHA-512 checksum file.
Before extracting Maven, it computes the local SHA-512 digest of the downloaded archive and compares it with the published checksum. If the two values do not match, the script stops immediately.
The checksum parsing is intentionally tolerant: instead of depending on one exact checksum-file format, the script scans the file for a 128-character hexadecimal token, which corresponds to a SHA-512 digest.
For computing the local checksum, the script supports several common tools:
sha512sumshasumopenssl
This makes the script usable on different Unix-like systems without assuming one specific checksum command.
Idempotency
The script is safe to run more than once.
If the requested Maven version is already installed and contains an executable mvn, the script does not download or extract it again. It still refreshes the stable symlink and the command links, but it avoids unnecessary network access.
When installation is needed, everything is done first in a temporary directory. The archive is downloaded there, the checksum is verified there, and Maven is extracted there. Only after the extracted archive looks valid is it moved into the final installation directory.
That keeps partially downloaded or invalid archives away from the final destination.
Stable symlinks
The script creates a stable Maven symlink:
|
1 2 |
~/.local/opt/maven |
pointing to the selected versioned installation:
|
1 2 |
~/.local/opt/apache-maven-3.9.16 |
Then it creates command symlinks in ~/.local/bin:
|
1 2 3 |
~/.local/bin/mvn ~/.local/bin/mvnDebug |
These point through the stable Maven symlink instead of directly to the versioned directory.
That means the command links do not need to change structurally when Maven is upgraded. The maven symlink changes, and mvn follows it.
The script is also conservative: it overwrites existing symlinks, but it does not overwrite existing non-symlink files. If a real file or directory already exists where the script wants to create a symlink, the script either keeps it or stops with an error.
PATH handling
At the end, the script checks whether ~/.local/bin is already in PATH.
If it is not, the script does not automatically modify shell startup files. It simply prints a reminder to add:
|
1 2 |
export PATH="$HOME/.local/bin:$PATH" |
I prefer this because installation scripts should not unexpectedly edit shell configuration files. The dotfiles themselves are the right place to manage PATH.
Why not use the package manager?
Installing Maven from the system package manager is perfectly fine in many cases.
For my dotfiles, however, I prefer this script because it gives me:
- a specific upstream Maven version;
- the same version across different machines;
- a user-local installation;
- no dependency on distribution-specific package names;
- a simple upgrade mechanism through chezmoi;
- checksum verification before installation.
This approach is not meant to replace the package manager for everything. It is just a good fit for development tools where I care about the exact upstream version.
Final result
After running:
|
1 2 |
chezmoi apply |
Maven is available as:
|
1 2 |
mvn --version |
and the installation is fully contained under my home directory.
The important paths are:
|
1 2 3 4 |
~/.local/opt/apache-maven-3.9.16 ~/.local/opt/maven ~/.local/bin/mvn |
This keeps the setup explicit, reproducible, and easy to update across machines.
Here’s an example updating the dotfiles on a machine after updating the Maven version:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Fast-forward .chezmoiscripts/run_onchange_after_20-install-maven.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) Downloading Maven 3.9.16... % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 8.84M 100 8.84M 0 0 13.43M 0 0 Downloading SHA-512 checksum... % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 128 100 128 0 0 514 0 0 Extracting Maven 3.9.16... Installed Maven 3.9.16 at /home/bettini/.local/opt/apache-maven-3.9.16. Maven 3.9.16 is available at: /home/bettini/.local/opt/maven/bin/mvn |
Happy dotfiles! 😉