diff --git a/docs/CPMUtil.md b/docs/CPMUtil.md index 7afaf3e38f..52aa4aa722 100644 --- a/docs/CPMUtil.md +++ b/docs/CPMUtil.md @@ -208,27 +208,15 @@ CI packages use `--` 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/` - - `tools/cpmutil.sh package fetch ` - - `cd .cache/cpm//` -- 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/`, 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 ` +- Make any necessary modifications to the package source. + - You can access the package source directory via `tools/cpmutil.sh package dir `. +- Create the patch: `tools/cpmutil.sh package patch ` + - 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 diff --git a/tools/cpm/common.sh b/tools/cpm/common.sh index 2844c1e5f5..09da6358be 100755 --- a/tools/cpm/common.sh +++ b/tools/cpm/common.sh @@ -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 diff --git a/tools/cpm/format.sh b/tools/cpm/format.sh index 58f95a7456..c1452c4cb2 100755 --- a/tools/cpm/format.sh +++ b/tools/cpm/format.sh @@ -5,3 +5,5 @@ jq --indent 4 -S cpmfile.json.new mv cpmfile.json.new cpmfile.json + +# TODO: Run some sanity checks e.g. patches exist, etc. diff --git a/tools/cpm/package.sh b/tools/cpm/package.sh index b6aa5ac236..bf2f3feaa7 100755 --- a/tools/cpm/package.sh +++ b/tools/cpm/package.sh @@ -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 "$@" diff --git a/tools/cpm/package/dir.sh b/tools/cpm/package/dir.sh new file mode 100755 index 0000000000..82a220509d --- /dev/null +++ b/tools/cpm/package/dir.sh @@ -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 </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 <&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" diff --git a/tools/cpm/package/reset.sh b/tools/cpm/package/reset.sh new file mode 100755 index 0000000000..6e98c00d48 --- /dev/null +++ b/tools/cpm/package/reset.sh @@ -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 <&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 +} diff --git a/tools/cpm/package/vars.sh b/tools/cpm/package/vars.sh index 32161e4454..dd497407c8 100755 --- a/tools/cpm/package/vars.sh +++ b/tools/cpm/package/vars.sh @@ -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 diff --git a/tools/cpmutil.sh b/tools/cpmutil.sh index 68459c7c78..6c18e6999b 100755 --- a/tools/cpmutil.sh +++ b/tools/cpmutil.sh @@ -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