Add patch functionality

Signed-off-by: crueter <crueter@eden-emu.dev>
This commit is contained in:
crueter 2026-06-25 19:59:41 -04:00
parent b288b2d0ad
commit cfcfa52207
No known key found for this signature in database
GPG key ID: 425ACD2D4830EBC6
11 changed files with 476 additions and 137 deletions

View file

@ -208,27 +208,15 @@ CI packages use `<platform>-<architecture>-<version>` unconditionally.
## Addendum: Making Patches
CPMUtil currently lacks functionality to make patches; these will be added in the future. For now, here's how to go about it; you should familiarize yourself with CPMUtil's shell tooling, and Git, first.
CPMUtil has a dedicated command for making patches. You're recommended to have Git and a command line editor installed, but CPMUtil is able to work without either. To do so, follow these steps, noting your package's JSON key:
- Clean-fetch your package:
- `rm -rf .cache/cpm/<lowercase package name>`
- `tools/cpmutil.sh package fetch <package JSON key>`
- `cd .cache/cpm/<lowercase package name>/<version>`
- Now, initialize a Git repository and commit:
- `git init`
- `git add .`
- `git commit -m "Initial Commit"`
- Now, make any changes you need within the source directory.
- Then, create your patch:
- `git commit -am "short summary of patch"`
- You may optionally choose to use `-av` instead to use your configured command-line editor. This also allows you to add a lengthy description.
- `git format-patch -1 HEAD`
- Your patch is now stored in `0001-short-summary-of-patch.patch` or similar.
- Copy this patch to the root of your repository.
- Rename the patch if needed, taking special note to ensure the starting number is correct
- For instance, if you already have 2 other patches, it should be renamed to `0003-...`
- Move the patch to `.patch/<package-json-key>`, creating the directory if it doesn't exist.
- Add your patch filename to the corresponding JSON definition.
- Clean-fetch your package: `tools/cpmutil.sh package reset <package>`
- Make any necessary modifications to the package source.
- You can access the package source directory via `tools/cpmutil.sh package dir <package>`.
- Create the patch: `tools/cpmutil.sh package patch <package>`
- Follow the on-screen prompts. If you have Git installed, an editor will be opened so you can type your commit message. If not, just type a one-line description.
And you're done! CPMUtil will automatically create and name the patch, and add it to the list of patches in the JSON definition.
## Addendum: Package Identification Lists

View file

@ -3,15 +3,43 @@
# SPDX-FileCopyrightText: Copyright 2026 crueter
# SPDX-License-Identifier: LGPL-3.0-or-later
: "${CPM_SOURCE_CACHE:=$PWD/.cache/cpm}"
: "${CPMUTIL_PATCH_DIR:=$PWD/.patch}"
# TODO: cache cpmfile defs?
cmd_exists() {
command -v "$1" >/dev/null 2>&1
}
must_install() {
for cmd in "$@"; do
command -v "$cmd" >/dev/null 2>&1 || { echo "-- $cmd must be installed" && exit 1; }
cmd_exists "$cmd" || { echo "-- $cmd must be installed" && exit 1; }
done
}
must_install jq find mktemp tar 7z unzip sha512sum git patch curl
# Random integer between 100000 and 999999
_randint() {
awk 'BEGIN { srand(); print int(100000 + rand() * 900000) }'
}
LIBS=$(jq -j 'keys_unsorted | join(" ")' cpmfile.json)
# Use mktemp if available, use a local temp dir otherwise
make_temp_dir() {
if cmd_exists mktemp; then
mktemp -d
else
TMP="$PWD/.cpm/tmp-$(_randint)"
mkdir -p "$TMP"
echo "$TMP"
fi
}
export LIBS
# must_install jq find mktemp tar 7z unzip sha512sum git patch curl
if [ ! -s cpmfile.json ]; then
# TODO: actually make it a no-op
echo "-- Warning: cpmfile.json does not exist or is empty, most commands will be no-ops"
else
LIBS=$(jq -j 'keys_unsorted | join(" ")' cpmfile.json)
export LIBS
fi

View file

@ -5,3 +5,5 @@
jq --indent 4 -S <cpmfile.json >cpmfile.json.new
mv cpmfile.json.new cpmfile.json
# TODO: Run some sanity checks e.g. patches exist, etc.

View file

@ -20,6 +20,9 @@ Commands:
version Change the version of a package
which Check if a package is defined
download Get the download URL for a package
dir Get the local directory for a package
reset Reset a fetched package to its original state
patch Create an in-tree patch based on local modifications
EOF
@ -31,7 +34,7 @@ export SCRIPTS
while :; do
case "$1" in
hash | update | fetch | add | rm | version | which | download)
hash | update | fetch | add | rm | version | which | download | reset | patch | dir)
cmd="$1"
shift
"$SCRIPTS/$cmd".sh "$@"

57
tools/cpm/package/dir.sh Executable file
View file

@ -0,0 +1,57 @@
#!/bin/sh -e
# SPDX-FileCopyrightText: Copyright 2026 crueter
# SPDX-License-Identifier: LGPL-3.0-or-later
# shellcheck disable=SC1091
. "$SCRIPTS"/../common.sh
usage() {
cat <<EOF
Usage: cpmutil.sh package dir [-a|--all] [PACKAGE]...
Get the local directory for the specified packages.
Options:
-a, --all Operate on all packages in this project.
EOF
exit 0
}
while :; do
case "$1" in
-a | --all) ALL=1 ;;
-h | --help) usage ;;
"$0") break ;;
"") break ;;
*) packages="$packages $1" ;;
esac
shift
done
[ "$ALL" != 1 ] || packages="${LIBS:-$packages}"
[ -n "$packages" ] || usage
for pkg in $packages; do
unset JSON
export PACKAGE="$pkg"
# shellcheck disable=SC1091
. "$SCRIPTS"/vars.sh
# TODO: common get dir func
if [ "$CI" = true ]; then
dir="${CPM_SOURCE_CACHE}/${LOWER_PACKAGE}"
else
dir="${CPM_SOURCE_CACHE}/${LOWER_PACKAGE}/${KEY}"
fi
echo "-- $pkg: $dir"
if [ ! -d "$dir" ]; then
echo "-- * Warning: directory does not exist. Use fetch or reset to create it"
fi
done

View file

@ -3,110 +3,8 @@
# SPDX-FileCopyrightText: Copyright 2026 crueter
# SPDX-License-Identifier: LGPL-3.0-or-later
: "${CPM_SOURCE_CACHE:=$PWD/.cache/cpm}"
mkdir -p "$CPM_SOURCE_CACHE"
ROOTDIR="$PWD"
TMP=$(mktemp -d)
download_package() {
FILENAME=$(basename "$DOWNLOAD")
OUTFILE="$TMP/$FILENAME"
LOWER_PACKAGE=$(echo "$PACKAGE_NAME" | tr '[:upper:]' '[:lower:]')
OUTDIR="${CPM_SOURCE_CACHE}/${LOWER_PACKAGE}/${KEY}"
[ -d "$OUTDIR" ] && return
curl "$DOWNLOAD" -sS -L -o "$OUTFILE"
ACTUAL_HASH=$(sha512sum "$OUTFILE" | cut -d" " -f1)
[ "$ACTUAL_HASH" != "$HASH" ] && echo "!! $FILENAME did not match expected hash; expected $HASH but got $ACTUAL_HASH" && exit 1
TMPDIR="$TMP/extracted"
mkdir -p "$OUTDIR"
mkdir -p "$TMPDIR"
PREVDIR="$PWD"
mkdir -p "$TMPDIR"
cd "$TMPDIR"
case "$FILENAME" in
*.7z)
7z x "$OUTFILE" >/dev/null
;;
*.tar*)
tar xf "$OUTFILE" >/dev/null
;;
*.zip)
unzip "$OUTFILE" >/dev/null
;;
esac
# basically if only one real item exists at the top we just move everything from there
# since github and some vendors hate me
DIRS=$(find . -maxdepth 1 -type d -o -type f)
# thanks gnu
if [ "$(echo "$DIRS" | wc -l)" -eq 2 ]; then
SUBDIR=$(find . -maxdepth 1 -type d -not -name ".")
mv "$SUBDIR"/* "$OUTDIR"
mv "$SUBDIR"/.* "$OUTDIR" 2>/dev/null || true
rmdir "$SUBDIR"
else
mv ./* "$OUTDIR"
mv ./.* "$OUTDIR" 2>/dev/null || true
fi
cd "$OUTDIR"
if echo "$JSON" | grep -e "patches" >/dev/null; then
PATCHES=$(echo "$JSON" | jq -r '.patches | join(" ")')
for patch in $PATCHES; do
patch --binary -p1 <"$ROOTDIR/.patch/$PACKAGE/$patch"
done
fi
cd "$PREVDIR"
}
ci_package() {
[ "$REPO" != null ] || { echo "-- ! No repo defined" && return; }
echo "-- CI package $PACKAGE_NAME"
for platform in \
windows-amd64 windows-arm64 \
mingw-amd64 mingw-arm64 \
android-aarch64 android-x86_64 \
linux-amd64 linux-aarch64 \
macos-universal ios-aarch64; do
echo "-- * platform $platform"
case $DISABLED in
*"$platform"*)
echo "-- * -- disabled"
continue
;;
esac
FILENAME="${NAME}-${platform}-${VERSION}.${EXT}"
DOWNLOAD="https://$GIT_HOST/${REPO}/releases/download/v${VERSION}/${FILENAME}"
KEY="$platform-$VERSION"
LOWER_PACKAGE=$(echo "$PACKAGE_NAME" | tr '[:upper:]' '[:lower:]')
OUTDIR="${CPM_SOURCE_CACHE}/${LOWER_PACKAGE}/${KEY}"
[ -d "$OUTDIR" ] && continue
HASH_URL="${DOWNLOAD}.sha512sum"
HASH=$(curl "$HASH_URL" -sS -q -L -o -)
download_package
done
}
# shellcheck disable=SC1091
. "$SCRIPTS/util/fetch.sh"
usage() {
cat <<EOF
@ -142,12 +40,5 @@ for PACKAGE in $packages; do
# shellcheck disable=SC1091
. "$SCRIPTS"/vars.sh
if [ "$CI" = "true" ]; then
ci_package
else
echo "-- Downloading regular package $PACKAGE, with key $KEY, from $DOWNLOAD"
download_package
fi
fetch_package
done
rm -rf "$TMP"

173
tools/cpm/package/patch.sh Executable file
View file

@ -0,0 +1,173 @@
#!/bin/sh -e
# SPDX-FileCopyrightText: Copyright 2026 crueter
# SPDX-License-Identifier: LGPL-3.0-or-later
# shellcheck disable=SC1091
. "$SCRIPTS"/../common.sh
RETURN=0
usage() {
cat <<EOF
Usage: cpmutil.sh package patch [PACKAGE]
Create an in-tree patch for the specified package.
EOF
exit "$RETURN"
}
die() {
echo "-- $*" >&2
RETURN=1 usage
}
while :; do
case "$1" in
-h | --help) usage ;;
"$0") break ;;
"") break ;;
*)
if [ -n "$PACKAGE" ]; then
die "You may only specify one package."
else
PACKAGE="$1"
fi
;;
esac
shift
done
[ -n "$PACKAGE" ] || usage
unset JSON
export PACKAGE
# shellcheck disable=SC1091
. "$SCRIPTS"/vars.sh
if [ "$CI" = true ]; then
die "CI packages do not support in-tree patching"
fi
patch_dir="$PWD/.patch/$PACKAGE"
TMP=$(make_temp_dir)
tmp_package="${TMP}/${LOWER_PACKAGE}/${KEY}"
# First fetch the package...
# shellcheck disable=SC1091
. "$SCRIPTS"/util/fetch.sh
local_dir="$CPM_SOURCE_CACHE/$LOWER_PACKAGE/$KEY"
if [ ! -d "$local_dir" ]; then
echo "-- $PACKAGE_NAME is not fetched locally" >&2
exit 1
fi
src_dir="$tmp_package/$LOWER_PACKAGE/$KEY"
CPM_SOURCE_CACHE="$tmp_package" fetch_package
mkdir -p "$patch_dir"
existing=$(find "$patch_dir" -maxdepth 1 -type f -name '[0-9][0-9][0-9][0-9]-*.patch' 2>/dev/null | sort | tail -1)
if [ -n "$existing" ]; then
last_num=$(basename "$existing" | cut -c1-4)
last_num=$(echo "$last_num" | sed 's/^0*//')
last_num="${last_num:-0}"
next_num=$(printf "%04d" $((last_num + 1)))
else
next_num="0001"
fi
echo "-- Creating patch for $PACKAGE..."
orig_dir="$PWD"
if cmd_exists git; then
cd "$src_dir"
git init >/dev/null 2>&1
git add -A >/dev/null 2>&1
git commit -m "base" >/dev/null 2>&1
git --work-tree="$local_dir" add -A #>/dev/null 2>&1
if git diff --cached --quiet; then
echo "-- No differences found between local and source"
cd "$orig_dir"
rm -rf "$TMP"
exit 0
fi
if ! git commit -v; then
echo "-- Patch cancelled" >&2
cd "$orig_dir"
rm -rf "$TMP"
exit 1
fi
description=$(git log -1 --format="%s")
git format-patch -1 HEAD --stdout >"$patch_dir/tmp.patch"
rm -rf .git
cd "$orig_dir"
unset orig_dir
else
if diff -ruN "$src_dir" "$local_dir" >/dev/null 2>&1; then
echo "-- No differences found between local and source"
rm -rf "$TMP"
exit 0
fi
printf -- "-- Enter a patch description: "
if ! read -r description; then
echo "" >&2
echo "-- Patch cancelled" >&2
rm -rf "$TMP"
exit 1
fi
mkdir -p "$TMP/patchdir/a" "$TMP/patchdir/b"
cp -r "$src_dir/." "$TMP/patchdir/a/"
cp -r "$local_dir/." "$TMP/patchdir/b/"
cd "$TMP/patchdir"
{
echo "Subject: [PATCH] $description" | fold -s -w 80
echo "---"
diff -ruN a b || true
} >"$patch_dir/tmp.patch"
cd "$orig_dir"
unset orig_dir
fi
rm -rf "$TMP"
if [ ! -s "$patch_dir/tmp.patch" ]; then
rm -f "$patch_dir/tmp.patch"
echo "-- No differences found between local and source for $PACKAGE"
exit 0
fi
name_part=$(printf "%s" "$description" | sed 's/^[0-9]\{4\}-//' | sed 's/ /-/g; s/[^a-zA-Z0-9-]//g; s/--*/-/g; s/^-//; s/-$//')
[ -n "$name_part" ] || name_part="patch"
# max is 60 chars, minus 5 for number, minus 6 for patch suffix
max_name_len=49
name_part=$(printf "%s" "$name_part" | cut -c1-"$max_name_len" | sed 's/-$//')
patch_name="${next_num}-${name_part}.patch"
mv "$patch_dir/tmp.patch" "$patch_dir/$patch_name"
NEW_JSON=$(echo "$JSON" | jq ".patches += [\"$patch_name\"]")
"$SCRIPTS"/util/replace.sh "$PACKAGE" "$NEW_JSON"
echo "-- Patch created at $patch_dir/$patch_name"

56
tools/cpm/package/reset.sh Executable file
View file

@ -0,0 +1,56 @@
#!/bin/sh -e
# SPDX-FileCopyrightText: Copyright 2026 crueter
# SPDX-License-Identifier: LGPL-3.0-or-later
# shellcheck disable=SC1091
. "$SCRIPTS"/../common.sh
usage() {
cat <<EOF
Usage: cpmutil.sh package reset [a|--all] [PACKAGE]...
Reset a locally fetched package to its original state.
This is most useful for dropping any changes you've made.
EOF
exit 0
}
while :; do
case "$1" in
-h | --help) usage ;;
-a | --all) ALL=1 ;;
"$0") break ;;
"") break ;;
*) packages="$packages $1" ;;
esac
shift
done
[ "$ALL" != 1 ] || packages="${LIBS:-$packages}"
[ -n "$packages" ] || usage
for PACKAGE in $packages; do
unset JSON
export PACKAGE
# shellcheck disable=SC1091
. "$SCRIPTS"/vars.sh
if [ "$CI" = true ]; then
dir="${CPM_SOURCE_CACHE}/${LOWER_PACKAGE}"
else
dir="${CPM_SOURCE_CACHE}/${LOWER_PACKAGE}/${KEY}"
fi
echo "-- Removing $dir"
rm -rf "$dir"
# shellcheck disable=SC1091
. "$SCRIPTS"/util/fetch.sh
fetch_package
done

134
tools/cpm/package/util/fetch.sh Executable file
View file

@ -0,0 +1,134 @@
#!/bin/sh -e
# SPDX-FileCopyrightText: Copyright 2026 crueter
# SPDX-License-Identifier: LGPL-3.0-or-later
# shellcheck disable=SC1091
. "$SCRIPTS"/../common.sh
download_package() {
mkdir -p "$CPM_SOURCE_CACHE"
tmp=$(make_temp_dir)
FILENAME=$(basename "$DOWNLOAD")
OUTFILE="$tmp/$FILENAME"
OUTDIR="${CPM_SOURCE_CACHE}/${LOWER_PACKAGE}/${KEY}"
if [ -d "$OUTDIR" ]; then return; fi
curl "$DOWNLOAD" -sS -L -o "$OUTFILE"
ACTUAL_HASH=$(sha512sum "$OUTFILE" | cut -d" " -f1)
if [ "$ACTUAL_HASH" != "$HASH" ]; then
echo "!! $FILENAME did not match expected hash; expected $HASH but got $ACTUAL_HASH" >&2
exit 1
fi
TMPDIR="$tmp/extracted"
mkdir -p "$OUTDIR"
mkdir -p "$TMPDIR"
PREVDIR="$PWD"
mkdir -p "$TMPDIR"
cd "$TMPDIR"
case "$FILENAME" in
*.7z)
must_install 7z
7z x "$OUTFILE" >/dev/null
;;
*.tar*)
# TODO: Extensions
must_install tar
tar xf "$OUTFILE" >/dev/null
;;
*.zip)
must_install unzip
unzip "$OUTFILE" >/dev/null
;;
esac
# basically if only one real item exists at the top we just move everything from there
# since github and some vendors hate me
DIRS=$(find . -maxdepth 1 -type d -o -type f)
# thanks gnu
if [ "$(echo "$DIRS" | wc -l)" -eq 2 ]; then
SUBDIR=$(find . -maxdepth 1 -type d -not -name ".")
mv "$SUBDIR"/* "$OUTDIR"
mv "$SUBDIR"/.* "$OUTDIR" 2>/dev/null || true
rmdir "$SUBDIR"
else
mv ./* "$OUTDIR"
mv ./.* "$OUTDIR" 2>/dev/null || true
fi
cd "$OUTDIR"
# TODO: Common custom patch/source cache dirs
if echo "$JSON" | grep -e "patches" >/dev/null; then
PATCHES=$(echo "$JSON" | jq -r '.patches | join(" ")')
for patch in $PATCHES; do
abs_patch="$CPMUTIL_PATCH_DIR/$PACKAGE/$patch"
if [ ! -f "$abs_patch" ]; then
echo "-- * Attempted to apply nonexistent patch $patch!"
continue
fi
echo "-- * Applying patch $patch"
patch --binary -p1 <"$abs_patch"
done
fi
cd "$PREVDIR"
rm -rf "$tmp"
}
# TODO: individual platform fetch?
ci_package() {
[ "$REPO" != null ] || { echo "-- ! No repo defined" && return; }
mkdir -p "$CPM_SOURCE_CACHE"
echo "-- CI package $PACKAGE_NAME"
for platform in \
windows-amd64 windows-arm64 \
mingw-amd64 mingw-arm64 \
android-aarch64 android-x86_64 \
linux-amd64 linux-aarch64 \
macos-universal ios-aarch64; do
echo "-- * platform $platform"
case $DISABLED in
*"$platform"*)
echo "-- * -- disabled"
continue
;;
esac
FILENAME="${NAME}-${platform}-${VERSION}.${EXT}"
DOWNLOAD="https://$GIT_HOST/${REPO}/releases/download/v${VERSION}/${FILENAME}"
KEY="$platform-$VERSION"
OUTDIR="${CPM_SOURCE_CACHE}/${LOWER_PACKAGE}/${KEY}"
[ -d "$OUTDIR" ] && continue
HASH_URL="${DOWNLOAD}.sha512sum"
HASH=$(curl "$HASH_URL" -sS -q -L -o -)
download_package
done
}
fetch_package() {
if [ "$CI" = "true" ]; then
ci_package
else
echo "-- Downloading regular package $PACKAGE, with key $KEY, from $DOWNLOAD"
download_package
fi
}

View file

@ -55,7 +55,11 @@ PACKAGE_NAME=$(value "package")
GIT_HOST=$(value "git_host")
[ "$GIT_HOST" != null ] || GIT_HOST=github.com
# used for cache key
LOWER_PACKAGE=$(echo "$PACKAGE_NAME" | tr '[:upper:]' '[:lower:]')
export PACKAGE_NAME
export LOWER_PACKAGE
export REPO
export CI
export GIT_HOST

View file

@ -38,6 +38,9 @@ Package commands:
version Change the version of a package
which Check if a package is defined
download Get the download URL for a package
dir Get the local directory for a package
reset Reset a fetched package to its original state
patch Create an in-tree patch based on local modifications
EOF