SPS · log · files · refs · tags · releases · people · languages · GitHub · commit
8fbfde3
src: add conservative Git and directory repository handling
Parents
Stat
.github/workflows/ci.yml | 24 +++++ .gitignore | 2 +- Makefile | 99 +++++++++++++++++++- bin/mkpkg | 30 ++++-- bin/sget | 8 +- bin/src | 237 +++++++++++++++++++++++++++++++++++++++-------- examples/README | 10 +- examples/repos.conf | 13 ++- examples/sps.conf | 1 + lib/common.sh | 212 +++++++++++++++++++++++++++++++++++++++--- lib/config.awk | 19 ++-- lib/deps.awk | 41 +++++++- lib/index.awk | 11 ++- lib/recipe.awk | 26 +++++- lib/relations.awk | 17 ++-- lib/repository.awk | 105 ++++++++++++++++----- man/mkpkg.1 | 46 ++++++++- man/sps.conf.5 | 34 ++++++- man/src.1 | 33 +++++-- 19 files changed, 840 insertions(+), 128 deletions(-)
Diff
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..471632a
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,24 @@
+name: check
+
+on:
+ push:
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out source
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Run POSIX shell and AWK tests
+ shell: sh
+ run: make check
+ - name: Check tagged release archive
+ if: startsWith(github.ref, 'refs/tags/v')
+ shell: sh
+ run: make release-check
diff --git a/.gitignore b/.gitignore
index eea5120..3f084a0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,4 @@
*.pkg.tar.zst
*.tmp
.sps-*
-
+/dist/
diff --git a/Makefile b/Makefile
index a08bda7..547f49b 100644
--- a/Makefile
+++ b/Makefile
@@ -9,7 +9,8 @@ DESTDIR =
PROGRAMS = src sget mkpkg pkin pkdel pkstat pkcheck pkmark
AWK_LIBS = common.awk config.awk db.awk deps.awk index.awk package.awk recipe.awk repository.awk relations.awk version.awk
-.PHONY: all check lint install install-config clean
+.PHONY: all check lint check-version source-archive release-check \
+ install install-config clean
all:
@printf '%s\n' "SPS is interpreted; run 'make check' or 'make install'."
@@ -32,6 +33,101 @@ lint:
@awk -f lib/common.awk -f lib/config.awk </dev/null >/dev/null
@awk -f lib/version.awk </dev/null >/dev/null
+check-version:
+ @LC_ALL=C awk '\
+ NR == 1 && $$0 ~ /^[0-9]+\.[0-9]+\.[0-9]+$$/ { valid = 1 } \
+ END { exit !(NR == 1 && valid) } \
+ ' VERSION || { \
+ printf '%s\n' 'VERSION must contain one X.Y.Z release number' >&2; \
+ exit 1; \
+ }
+
+# Build from the committed tree so untracked files and local edits can never
+# enter a published source archive. gzip -n omits its timestamp and filename.
+source-archive: check-version
+ @set -eu; \
+ version=$$(sed -n '1p' VERSION); \
+ for tool in git gzip mktemp; do \
+ command -v "$$tool" >/dev/null 2>&1 || { \
+ printf 'source-archive: required tool not found: %s\n' "$$tool" >&2; \
+ exit 1; \
+ }; \
+ done; \
+ git rev-parse --is-inside-work-tree >/dev/null 2>&1 || { \
+ printf '%s\n' 'source-archive: not inside a Git work tree' >&2; \
+ exit 1; \
+ }; \
+ mkdir -p dist; \
+ archive="dist/sps-$$version.tar.gz"; \
+ tmp_tar=$$(mktemp "$${TMPDIR:-/tmp}/sps-source.XXXXXX"); \
+ tmp_archive="$$archive.tmp"; \
+ trap 'rm -f "$$tmp_tar" "$$tmp_archive"' 0 HUP INT TERM; \
+ git archive --format=tar --prefix="sps-$$version/" \
+ --output="$$tmp_tar" HEAD; \
+ gzip -n -9 -c "$$tmp_tar" >"$$tmp_archive"; \
+ mv "$$tmp_archive" "$$archive"; \
+ printf '%s\n' "$$archive"
+
+# Run this after creating vVERSION and before pushing the tag. It verifies
+# that the tag, committed VERSION, work tree, and reproducible archive agree.
+release-check: check source-archive
+ @set -eu; \
+ version=$$(sed -n '1p' VERSION); \
+ tag="v$$version"; \
+ archive="dist/sps-$$version.tar.gz"; \
+ prefix="sps-$$version/"; \
+ if [ -n "$$(git status --porcelain --untracked-files=normal)" ]; then \
+ printf '%s\n' 'release-check: Git work tree is not clean' >&2; \
+ exit 1; \
+ fi; \
+ git diff --check; \
+ tag_commit=$$(git rev-parse -q --verify "$$tag^{commit}") || { \
+ printf 'release-check: tag %s does not exist\n' "$$tag" >&2; \
+ exit 1; \
+ }; \
+ [ "$$(git cat-file -t "$$tag")" = tag ] || { \
+ printf 'release-check: tag %s is not annotated\n' "$$tag" >&2; \
+ exit 1; \
+ }; \
+ head_commit=$$(git rev-parse HEAD); \
+ [ "$$tag_commit" = "$$head_commit" ] || { \
+ printf 'release-check: tag %s does not point to HEAD\n' "$$tag" >&2; \
+ exit 1; \
+ }; \
+ gzip -t "$$archive"; \
+ members=$$(mktemp "$${TMPDIR:-/tmp}/sps-members.XXXXXX"); \
+ repro_tar=$$(mktemp "$${TMPDIR:-/tmp}/sps-repro.XXXXXX"); \
+ repro_archive=$$(mktemp "$${TMPDIR:-/tmp}/sps-repro-gz.XXXXXX"); \
+ trap 'rm -f "$$members" "$$repro_tar" "$$repro_archive"' \
+ 0 HUP INT TERM; \
+ tar -tzf "$$archive" >"$$members"; \
+ awk -v prefix="$$prefix" '\
+ index($$0, prefix) != 1 { bad = 1 } \
+ END { exit bad || NR == 0 } \
+ ' "$$members" || { \
+ printf '%s\n' 'release-check: archive has an invalid member path' >&2; \
+ exit 1; \
+ }; \
+ for required in VERSION Makefile bin/src; do \
+ grep -F -x "$$prefix$$required" "$$members" >/dev/null || { \
+ printf 'release-check: archive is missing %s\n' "$$required" >&2; \
+ exit 1; \
+ }; \
+ done; \
+ archive_version=$$(tar -xOzf "$$archive" "$$prefix"VERSION); \
+ [ "$$archive_version" = "$$version" ] || { \
+ printf '%s\n' 'release-check: archive VERSION does not match' >&2; \
+ exit 1; \
+ }; \
+ git archive --format=tar --prefix="$$prefix" \
+ --output="$$repro_tar" HEAD; \
+ gzip -n -9 -c "$$repro_tar" >"$$repro_archive"; \
+ cmp "$$archive" "$$repro_archive" >/dev/null || { \
+ printf '%s\n' 'release-check: source archive is not reproducible' >&2; \
+ exit 1; \
+ }; \
+ printf 'release-check: %s is ready\n' "$$archive"
+
install:
install -d "$(DESTDIR)$(BINDIR)" "$(DESTDIR)$(LIBDIR)" \
"$(DESTDIR)$(MANDIR)/man1" "$(DESTDIR)$(MANDIR)/man5"
@@ -65,4 +161,3 @@ install-config:
clean:
@find . -type f \( -name '*.pkg.tar' -o -name '*.pkg.tar.gz' -o \
-name '*.pkg.tar.xz' -o -name '*.pkg.tar.zst' \) -exec rm -f {} \;
-
diff --git a/bin/mkpkg b/bin/mkpkg
index a06beae..ca04b67 100755
--- a/bin/mkpkg
+++ b/bin/mkpkg
@@ -183,6 +183,8 @@ command -v mktemp >/dev/null 2>&1 || die "mktemp is required"
recipe_dir=$(CDPATH= cd "$(dirname "$recipe")" 2>/dev/null && pwd) ||
die "cannot enter recipe directory"
recipe_file=$recipe_dir/$(basename "$recipe")
+sps_definition_sha256 "$recipe_file" >/dev/null ||
+ die_status 4 "invalid package definition support inputs"
default_arch=${SPS_ARCH:-}
if [ -z "$default_arch" ]; then
default_arch=$(uname -m 2>/dev/null) || die "cannot determine build architecture"
@@ -283,6 +285,18 @@ function replace(text, token, value, at, out) {
}
return out text
}
+function expand(value, pass, previous) {
+ for (pass = 1; pass <= 8; pass++) {
+ previous = value
+ value = replace(value, "${name}", value_for["name"])
+ value = replace(value, "${version}", value_for["version"])
+ value = replace(value, "${release}", value_for["release"])
+ value = replace(value, "${arch}", value_for["arch"])
+ if (value == previous)
+ break
+ }
+ return value
+}
NR == FNR {
key = $1
value = substr($0, index($0, "\t") + 1)
@@ -307,10 +321,7 @@ NR == FNR {
}
key = $1
value = substr($0, index($0, "\t") + 1)
- value = replace(value, "${name}", value_for["name"])
- value = replace(value, "${version}", value_for["version"])
- value = replace(value, "${release}", value_for["release"])
- value = replace(value, "${arch}", value_for["arch"])
+ value = expand(value)
print key "\t" value
}
END {
@@ -352,6 +363,12 @@ for field in name version release arch; do
die_status 4 "invalid $field '$value'"
;;
esac
+ elif [ "$field" = arch ]; then
+ case $value in
+ ''|*[!ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._+-]*)
+ die_status 4 "invalid $field '$value'"
+ ;;
+ esac
else
case $value in
''|*[!ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._+~-]*)
@@ -507,8 +524,9 @@ while IFS=' ' read -r source hash_record; do
done <"$source_pairs"
for support in files patches; do
- if [ -d "$recipe_dir/$support" ] || [ -L "$recipe_dir/$support" ]; then
- [ -d "$recipe_dir/$support" ] || die "$recipe_dir/$support is not a directory"
+ if [ -e "$recipe_dir/$support" ] || [ -L "$recipe_dir/$support" ]; then
+ [ -d "$recipe_dir/$support" ] && [ ! -L "$recipe_dir/$support" ] ||
+ die_status 4 "recipe $support must be a real directory"
if [ -e "$source_dir/$support" ] || [ -L "$source_dir/$support" ]; then
die_status 4 "source conflicts with recipe $support directory"
fi
diff --git a/bin/sget b/bin/sget
index b0c735b..5816423 100755
--- a/bin/sget
+++ b/bin/sget
@@ -380,10 +380,10 @@ sget_install()
[ -n "$sget_action" ] || continue
[ -f "$sget_recipe" ] ||
sps_die "$SPS_EX_NOTFOUND" "recipe for '$sget_package' is missing: $sget_recipe"
- sget_current_hash=$(sps_sha256 "$sget_recipe") ||
- sps_die "$SPS_EX_CHECKSUM" "cannot hash recipe for '$sget_package'"
- [ "$sget_current_hash" = "$sget_recipe_hash" ] ||
- sps_die "$SPS_EX_CHECKSUM" "recipe for '$sget_package' changed since indexing; run 'src update'"
+ sget_current_hash=$(sps_definition_sha256 "$sget_recipe") ||
+ sps_die "$SPS_EX_CHECKSUM" "cannot hash package definition for '$sget_package'"
+ [ "$sget_current_hash" = "$sget_recipe_hash" ] ||
+ sps_die "$SPS_EX_CHECKSUM" "package definition for '$sget_package' changed since indexing; run 'src update'"
sget_recipe_dir=${sget_recipe%/*}
sget_recipe_name=${sget_recipe##*/}
diff --git a/bin/src b/bin/src
index 3cac1aa..f91cc16 100755
--- a/bin/src
+++ b/bin/src
@@ -39,12 +39,150 @@ src_need_index()
src_cleanup_update()
{
- [ -n "${src_work:-}" ] || return
- rm -f "$src_work/repos.raw" "$src_work/repos" \
- "$src_work/recipes.unsorted" "$src_work/recipes" \
- "$src_work/packages.index"
- rmdir "$src_work" 2>/dev/null || :
- src_work=
+ if [ -n "${src_clone_work:-}" ]; then
+ rm -rf "$src_clone_work"
+ src_clone_work=
+ fi
+ if [ -n "${src_work:-}" ]; then
+ rm -f "$src_work/repos.raw" "$src_work/repos" \
+ "$src_work/recipes.unsorted" "$src_work/recipes" \
+ "$src_work/packages.index"
+ rmdir "$src_work" 2>/dev/null || :
+ src_work=
+ fi
+ sps_lock_release
+}
+
+src_git_verify_checkout()
+{
+ src_verify_checkout=$1
+ src_verify_source=$2
+
+ [ -d "$src_verify_checkout" ] && [ ! -L "$src_verify_checkout" ] ||
+ return 1
+ src_verify_top=$(git -C "$src_verify_checkout" rev-parse --show-toplevel \
+ 2>/dev/null) || return 1
+ src_verify_top=$(CDPATH= cd -P "$src_verify_top" 2>/dev/null && pwd -P) ||
+ return 1
+ src_verify_expected=$(CDPATH= cd -P "$src_verify_checkout" 2>/dev/null &&
+ pwd -P) || return 1
+ [ "$src_verify_top" = "$src_verify_expected" ] || return 1
+ src_verify_origin=$(git -C "$src_verify_checkout" remote get-url origin \
+ 2>/dev/null) || return 1
+ [ "$src_verify_origin" = "$src_verify_source" ]
+}
+
+src_sync_git()
+{
+ src_git_name=$1
+ src_git_source=$2
+ src_git_checkout=$3
+ sps_require_cmd git
+
+ src_git_parent=${src_git_checkout%/*}
+ [ -n "$src_git_parent" ] || src_git_parent=/
+ sps_mkdir "$src_git_parent"
+
+ if [ ! -e "$src_git_checkout" ] && [ ! -L "$src_git_checkout" ]; then
+ src_clone_work=$(sps_mktemp_dir "$src_git_parent" \
+ "clone-$src_git_name")
+ src_clone_checkout=$src_clone_work/repository
+ if ! git clone -- "$src_git_source" "$src_clone_checkout"; then
+ sps_die "$SPS_EX_NETWORK" \
+ "cannot clone repository '$src_git_name'"
+ fi
+ if ! src_git_verify_checkout "$src_clone_checkout" "$src_git_source"; then
+ sps_die "$SPS_EX_PACKAGE" \
+ "cloned repository '$src_git_name' failed checkout verification"
+ fi
+ if [ -e "$src_git_checkout" ] || [ -L "$src_git_checkout" ]; then
+ sps_die "$SPS_EX_CONFLICT" \
+ "repository checkout appeared while cloning: $src_git_checkout"
+ fi
+ if ! mv "$src_clone_checkout" "$src_git_checkout"; then
+ sps_die "$SPS_EX_PERMISSION" \
+ "cannot commit repository checkout: $src_git_checkout"
+ fi
+ rmdir "$src_clone_work" 2>/dev/null || :
+ src_clone_work=
+ printf "%s: cloned repository '%s' into %s\n" \
+ "$sps_prog" "$src_git_name" "$src_git_checkout" >&2
+ return 0
+ fi
+
+ if ! src_git_verify_checkout "$src_git_checkout" "$src_git_source"; then
+ if [ -d "$src_git_checkout" ] && [ ! -L "$src_git_checkout" ] &&
+ git -C "$src_git_checkout" rev-parse --is-inside-work-tree \
+ >/dev/null 2>&1; then
+ sps_die "$SPS_EX_CONFLICT" \
+ "repository '$src_git_name' origin does not match its configured source"
+ fi
+ sps_die "$SPS_EX_CONFLICT" \
+ "repository checkout is not the expected Git worktree: $src_git_checkout"
+ fi
+
+ src_git_dirty=$(git -C "$src_git_checkout" status --porcelain \
+ --untracked-files=normal) ||
+ sps_die "$SPS_EX_PACKAGE" \
+ "cannot inspect repository '$src_git_name'"
+ if [ -n "$src_git_dirty" ]; then
+ sps_die "$SPS_EX_CONFLICT" \
+ "repository '$src_git_name' has local modifications; refusing to update $src_git_checkout"
+ fi
+
+ if ! git -C "$src_git_checkout" fetch origin; then
+ sps_die "$SPS_EX_NETWORK" \
+ "cannot fetch repository '$src_git_name'"
+ fi
+
+ if ! src_git_branch=$(git -C "$src_git_checkout" symbolic-ref -q \
+ --short HEAD 2>/dev/null); then
+ printf "%s: repository '%s' is detached and remains pinned at %s\n" \
+ "$sps_prog" "$src_git_name" \
+ "$(git -C "$src_git_checkout" rev-parse --short HEAD)" >&2
+ return 0
+ fi
+ if ! src_git_upstream=$(git -C "$src_git_checkout" rev-parse \
+ --abbrev-ref --symbolic-full-name '@{upstream}' 2>/dev/null); then
+ printf "%s: repository '%s' branch '%s' has no upstream and remains pinned\n" \
+ "$sps_prog" "$src_git_name" "$src_git_branch" >&2
+ return 0
+ fi
+ case $src_git_upstream in
+ origin/*) ;;
+ *)
+ printf "%s: repository '%s' tracks '%s', not origin, and remains pinned\n" \
+ "$sps_prog" "$src_git_name" "$src_git_upstream" >&2
+ return 0
+ ;;
+ esac
+
+ src_git_head=$(git -C "$src_git_checkout" rev-parse HEAD) ||
+ sps_die "$SPS_EX_PACKAGE" "cannot read HEAD for repository '$src_git_name'"
+ src_git_upstream_head=$(git -C "$src_git_checkout" rev-parse \
+ "$src_git_upstream") ||
+ sps_die "$SPS_EX_PACKAGE" \
+ "cannot read upstream for repository '$src_git_name'"
+ if [ "$src_git_head" = "$src_git_upstream_head" ]; then
+ return 0
+ fi
+ if git -C "$src_git_checkout" merge-base --is-ancestor \
+ "$src_git_head" "$src_git_upstream_head"; then
+ if ! git -C "$src_git_checkout" merge --ff-only \
+ "$src_git_upstream" >&2; then
+ sps_die "$SPS_EX_CONFLICT" \
+ "cannot fast-forward repository '$src_git_name'"
+ fi
+ return 0
+ fi
+ if git -C "$src_git_checkout" merge-base --is-ancestor \
+ "$src_git_upstream_head" "$src_git_head"; then
+ printf "%s: repository '%s' is ahead of %s and was left unchanged\n" \
+ "$sps_prog" "$src_git_name" "$src_git_upstream" >&2
+ return 0
+ fi
+ sps_die "$SPS_EX_CONFLICT" \
+ "repository '$src_git_name' has diverged from $src_git_upstream; refusing to update"
}
src_update()
@@ -54,19 +192,24 @@ src_update()
exit "$SPS_EX_USAGE"
}
- sps_mkdir "$sps_index_dir"
- src_work=$(sps_mktemp_dir "$sps_index_dir" update)
- trap src_cleanup_update 0
- trap 'exit 129' 1
- trap 'exit 130' 2
- trap 'exit 131' 3
- trap 'exit 143' 15
+ sps_mkdir "$sps_index_dir"
+ src_work=
+ src_clone_work=
+ sps_lock_acquire "$SPS_CACHE" .src-update 'repository update' ||
+ sps_die "$SPS_EX_CONFLICT" "another repository update is active"
+ trap src_cleanup_update 0
+ trap 'exit 129' 1
+ trap 'exit 130' 2
+ trap 'exit 131' 3
+ trap 'exit 143' 15
+ src_work=$(sps_mktemp_dir "$sps_index_dir" update)
if ! sps_repo_lines > "$src_work/repos.raw"; then
sps_die "$SPS_EX_USAGE" "cannot read repository configuration"
fi
- if ! awk -v action=normalize -v program=src \
- -f "$SPS_LIBDIR/repository.awk" "$src_work/repos.raw" \
+ if ! awk -v action=normalize -v program=src \
+ -v repo_root="$SPS_REPO_ROOT" \
+ -f "$SPS_LIBDIR/repository.awk" "$src_work/repos.raw" \
> "$src_work/repos"; then
sps_die "$SPS_EX_USAGE" "invalid repository configuration"
fi
@@ -78,18 +221,20 @@ src_update()
src_package_count=0
src_failed=0
- while IFS="$sps_tab" read -r src_repo_name src_repo_path \
- src_repo_priority src_repo_order
- do
- [ -n "$src_repo_name" ] || continue
- case $src_repo_path in
- *://*|git@*:*)
- printf "%s: repository '%s' uses an unsupported remote location: %s\n" \
- "$sps_prog" "$src_repo_name" "$src_repo_path" >&2
- src_failed=1
- break
- ;;
- esac
+ while IFS="$sps_tab" read -r src_repo_kind src_repo_name \
+ src_repo_source src_repo_path src_repo_priority src_repo_order
+ do
+ [ -n "$src_repo_name" ] || continue
+ case $src_repo_kind in
+ git) src_sync_git "$src_repo_name" "$src_repo_source" "$src_repo_path" ;;
+ dir) ;;
+ *)
+ printf "%s: internal error: unsupported repository kind '%s'\n" \
+ "$sps_prog" "$src_repo_kind" >&2
+ src_failed=1
+ break
+ ;;
+ esac
if [ ! -d "$src_repo_path" ]; then
printf "%s: repository '%s' is not a local directory: %s\n" \
"$sps_prog" "$src_repo_name" "$src_repo_path" >&2
@@ -104,8 +249,11 @@ src_update()
break
}
- if ! find "$src_repo_absolute" -type f -name recipe -print \
- > "$src_work/recipes.unsorted"; then
+ if ! find "$src_repo_absolute" \
+ \( -type d \( -name .git -o -name files -o -name patches -o \
+ -name hooks \) -prune \) -o \
+ \( -type f -name recipe -print \) \
+ > "$src_work/recipes.unsorted"; then
printf "%s: cannot scan repository '%s': %s\n" \
"$sps_prog" "$src_repo_name" "$src_repo_absolute" >&2
src_failed=1
@@ -128,22 +276,22 @@ src_update()
src_failed=1
break
fi
- src_digest=$(sps_sha256 "$src_recipe") || {
- printf "%s: cannot hash recipe: %s\n" \
- "$sps_prog" "$src_recipe" >&2
+ src_digest=$(sps_definition_sha256 "$src_recipe") || {
+ printf "%s: cannot hash package definition: %s\n" \
+ "$sps_prog" "$src_recipe" >&2
src_failed=1
break
}
case $src_digest in
''|*[!0123456789abcdefABCDEF]*)
- printf "%s: invalid SHA-256 result for recipe: %s\n" \
+ printf "%s: invalid package definition SHA-256: %s\n" \
"$sps_prog" "$src_recipe" >&2
src_failed=1
break
;;
esac
if [ "${#src_digest}" -ne 64 ]; then
- printf "%s: invalid SHA-256 result for recipe: %s\n" \
+ printf "%s: invalid package definition SHA-256: %s\n" \
"$sps_prog" "$src_recipe" >&2
src_failed=1
break
@@ -170,11 +318,17 @@ src_update()
sps_die "$SPS_EX_PACKAGE" \
"repository update failed; previous index was preserved"
fi
- if ! awk -v action=validate -v program=src \
- -f "$SPS_LIBDIR/index.awk" "$src_work/packages.index"; then
+ if ! awk -v action=validate -v program=src \
+ -f "$SPS_LIBDIR/index.awk" "$src_work/packages.index"; then
sps_die "$SPS_EX_PACKAGE" \
- "generated repository index is malformed"
- fi
+ "generated repository index is malformed"
+ fi
+ if ! awk -v action=validate -v include_build=1 -v program=src \
+ -f "$SPS_LIBDIR/version.awk" -f "$SPS_LIBDIR/deps.awk" \
+ "$src_work/packages.index"; then
+ sps_die "$SPS_EX_DEPENDENCY" \
+ "generated repository dependency graph is invalid"
+ fi
if ! mv "$src_work/packages.index" "$sps_index"; then
sps_die "$SPS_EX_PERMISSION" "cannot commit repository index"
fi
@@ -202,9 +356,10 @@ src_repositories()
else
[ "$src_raw" -eq 0 ] || src_repo_action=status_raw
fi
- sps_repo_lines |
- awk -v action="$src_repo_action" -v program=src \
- -v index_file="$sps_index" \
+ sps_repo_lines |
+ awk -v action="$src_repo_action" -v program=src \
+ -v repo_root="$SPS_REPO_ROOT" \
+ -v index_file="$sps_index" \
-f "$SPS_LIBDIR/repository.awk"
}
diff --git a/examples/README b/examples/README
index 91365a4..7b26617 100644
--- a/examples/README
+++ b/examples/README
@@ -10,9 +10,15 @@ point for new recipes.
Local source paths are resolved from the recipe directory. Downloaded or copied
sources are available below $SRC. Tar sources are unpacked below $WORK; when an
archive contains one top-level directory, that directory becomes $WORK.
+Every source has a corresponding `hash sha256:...` record with exactly 64
+hexadecimal digits. Checksums cannot be skipped.
The install phase writes into $PKG. It must not install directly into the live
-root.
+root. At least one install record and a non-empty payload are required.
If files/ or patches/ exist beside the recipe, mkpkg copies them to $SRC/files
-and $SRC/patches.
+and $SRC/patches. Recipes apply patches and install local files explicitly.
+
+Use `arch any` only for architecture-independent packages. Compiled recipes can
+omit `arch` to use the configured target architecture. See
+`../PACKAGE-MAINTAINER.md` for the full recipe and test procedure.
diff --git a/examples/repos.conf b/examples/repos.conf
index b5e9260..7f8817b 100644
--- a/examples/repos.conf
+++ b/examples/repos.conf
@@ -1,5 +1,10 @@
-# Format: repo NAME PATH [PRIORITY]
+# Formats:
+# git NAME SOURCE [PRIORITY]
+# dir NAME ABSOLUTE_PATH [PRIORITY]
#
-# Example:
-# repo core /usr/src/core 100
-# repo local /usr/src/local 200
+# Official repositories are cloned below repo_root/NAME. Local trees are
+# indexed in place. Higher priority wins.
+#
+# dir local /usr/local/src/sps-local 200
+# git core https://github.com/RobertFlexx/sps-core.git 100
+# git extra https://github.com/RobertFlexx/sps-extra.git 80
diff --git a/examples/sps.conf b/examples/sps.conf
index 24f1dde..f1e0c5d 100644
--- a/examples/sps.conf
+++ b/examples/sps.conf
@@ -3,6 +3,7 @@
db /var/lib/sps
cache /var/cache/sps
build /var/tmp/sps
+repo_root /usr/src/sps
makejobs 1
compression auto
preserve etc
diff --git a/lib/common.sh b/lib/common.sh
index 0206ac4..fa164fe 100644
--- a/lib/common.sh
+++ b/lib/common.sh
@@ -108,6 +108,170 @@ sps_sha256()
fi
}
+# Hash every input which makes up one package definition. The manifest uses
+# only fixed, package-relative names, so moving an unchanged package directory
+# does not change its digest. files/ and patches/ are copied recursively by
+# mkpkg, making their directory and file modes observable build inputs too.
+sps_definition_sha256()
+(
+ sps_definition_input=${1-}
+ case $sps_definition_input in
+ '') sps_warn 'package definition recipe is missing'; return 1 ;;
+ */*)
+ sps_definition_parent=${sps_definition_input%/*}
+ sps_definition_leaf=${sps_definition_input##*/}
+ ;;
+ *)
+ sps_definition_parent=.
+ sps_definition_leaf=$sps_definition_input
+ ;;
+ esac
+ case $sps_definition_parent in
+ -*) sps_definition_parent=./$sps_definition_parent ;;
+ esac
+ sps_definition_dir=$(CDPATH= cd -P "$sps_definition_parent" 2>/dev/null &&
+ pwd -P) || {
+ sps_warn 'cannot enter package definition directory'
+ return 1
+ }
+ sps_definition_recipe=$sps_definition_dir/$sps_definition_leaf
+ if [ ! -f "$sps_definition_recipe" ] ||
+ [ -L "$sps_definition_recipe" ] ||
+ [ ! -r "$sps_definition_recipe" ]; then
+ sps_warn 'package definition recipe must be a readable regular file'
+ return 1
+ fi
+
+ sps_definition_tmp=$(mktemp -d \
+ "${TMPDIR:-/tmp}/.sps-definition.XXXXXX" 2>/dev/null) || {
+ sps_warn 'cannot create package definition hash workspace'
+ return 1
+ }
+ sps_definition_cleanup()
+ {
+ rm -f "$sps_definition_tmp/manifest" \
+ "$sps_definition_tmp/paths.raw" \
+ "$sps_definition_tmp/paths" \
+ "$sps_definition_tmp/unsafe-path" \
+ "$sps_definition_tmp/unsafe-type"
+ rmdir "$sps_definition_tmp" 2>/dev/null || :
+ }
+ trap 'sps_definition_cleanup' 0
+ trap 'exit 1' 1 2 3 15
+
+ sps_definition_hash_file()
+ {
+ sps_definition_value=$(sps_sha256 "$1") || return $?
+ sps_definition_value=$(printf '%s\n' "$sps_definition_value" |
+ LC_ALL=C tr 'ABCDEF' 'abcdef') || return $?
+ case $sps_definition_value in
+ ''|*[!0123456789abcdef]*) return 1 ;;
+ esac
+ [ "${#sps_definition_value}" -eq 64 ] || return 1
+ printf '%s\n' "$sps_definition_value"
+ }
+
+ sps_definition_manifest=$sps_definition_tmp/manifest
+ printf 'sps-package-definition\t1\n' >"$sps_definition_manifest" ||
+ return 1
+ sps_definition_hash=$(sps_definition_hash_file \
+ "$sps_definition_recipe") || {
+ sps_warn 'cannot hash package definition recipe'
+ return 1
+ }
+ printf 'file\t-\t%s\trecipe\n' "$sps_definition_hash" \
+ >>"$sps_definition_manifest" || return 1
+
+ cd "$sps_definition_dir" || return 1
+ for sps_definition_support in files patches hooks; do
+ if [ ! -e "$sps_definition_support" ] &&
+ [ ! -L "$sps_definition_support" ]; then
+ continue
+ fi
+ if [ ! -d "$sps_definition_support" ] ||
+ [ -L "$sps_definition_support" ]; then
+ sps_warn "recipe $sps_definition_support must be a real directory"
+ return 1
+ fi
+
+ rm -f "$sps_definition_tmp/unsafe-path" \
+ "$sps_definition_tmp/unsafe-type"
+ if ! find "$sps_definition_support" -exec sh -c '
+ marker=$1
+ shift
+ for path do
+ case $path in
+ *" "*|*"
+"*) : >"$marker"; exit 0 ;;
+ esac
+ done
+ ' sh "$sps_definition_tmp/unsafe-path" {} +; then
+ sps_warn "cannot inspect package definition $sps_definition_support"
+ return 1
+ fi
+ if [ -e "$sps_definition_tmp/unsafe-path" ]; then
+ sps_warn "package definition $sps_definition_support contains a tab or newline in a path"
+ return 1
+ fi
+ if ! find "$sps_definition_support" ! -type d ! -type f \
+ -exec sh -c ': >"$1"' sh \
+ "$sps_definition_tmp/unsafe-type" {} +; then
+ sps_warn "cannot inspect package definition $sps_definition_support"
+ return 1
+ fi
+ if [ -e "$sps_definition_tmp/unsafe-type" ]; then
+ sps_warn "package definition $sps_definition_support contains a symlink or special file"
+ return 1
+ fi
+ if ! find "$sps_definition_support" -print \
+ >"$sps_definition_tmp/paths.raw"; then
+ sps_warn "cannot list package definition $sps_definition_support"
+ return 1
+ fi
+ if ! LC_ALL=C sort "$sps_definition_tmp/paths.raw" \
+ >"$sps_definition_tmp/paths"; then
+ sps_warn "cannot sort package definition $sps_definition_support"
+ return 1
+ fi
+
+ while IFS= read -r sps_definition_path ||
+ [ -n "$sps_definition_path" ]; do
+ [ -n "$sps_definition_path" ] || continue
+ if [ -d "$sps_definition_path" ] &&
+ [ ! -L "$sps_definition_path" ]; then
+ sps_definition_mode=$(sps_mode_of \
+ "$sps_definition_path") || {
+ sps_warn "cannot read a $sps_definition_support directory mode"
+ return 1
+ }
+ printf 'directory\t%s\t-\t%s\n' \
+ "$sps_definition_mode" "$sps_definition_path" \
+ >>"$sps_definition_manifest" || return 1
+ elif [ -f "$sps_definition_path" ] &&
+ [ ! -L "$sps_definition_path" ]; then
+ sps_definition_mode=$(sps_mode_of \
+ "$sps_definition_path") || {
+ sps_warn "cannot read a $sps_definition_support file mode"
+ return 1
+ }
+ sps_definition_hash=$(sps_definition_hash_file \
+ "$sps_definition_path") || {
+ sps_warn "cannot hash a $sps_definition_support file"
+ return 1
+ }
+ printf 'file\t%s\t%s\t%s\n' "$sps_definition_mode" \
+ "$sps_definition_hash" "$sps_definition_path" \
+ >>"$sps_definition_manifest" || return 1
+ else
+ sps_warn "package definition $sps_definition_support changed while hashing"
+ return 1
+ fi
+ done <"$sps_definition_tmp/paths"
+ done
+
+ sps_definition_hash_file "$sps_definition_manifest"
+)
+
sps_validate_package_name()
{
case ${1-} in
@@ -208,6 +372,7 @@ sps_lock_acquire()
{
sps_lock_base=$1
sps_lock_name=${2:-.lock}
+ sps_lock_label=${3:-package database}
case $sps_lock_name in ''|*/*|.|..) return 1 ;; esac
sps_lock=$sps_lock_base/$sps_lock_name
sps_lock_owned=0
@@ -228,7 +393,7 @@ sps_lock_acquire()
return 1
fi
- sps_warn "removing stale package database lock from pid $sps_lock_pid"
+ sps_warn "removing stale $sps_lock_label lock '$sps_lock' from pid $sps_lock_pid"
rm -f "$sps_lock/pid" 2>/dev/null || return 1
rmdir "$sps_lock" 2>/dev/null || return 1
if mkdir "$sps_lock" 2>/dev/null; then
@@ -250,19 +415,35 @@ sps_lock_release()
sps_lock_owned=0
}
-# Print repo lines from the main config and repos.conf.
+# Print repository declarations without normalizing or truncating them. General
+# settings in sps.conf are ignored here; repos.conf is repository-only, so an
+# unknown declaration there is preserved for repository.awk to reject.
sps_repo_lines()
{
+ sps_repo_previous=
for sps_repo_file in "$SPS_CONFIG" "$SPS_REPOS_CONFIG"; do
[ -r "$sps_repo_file" ] || continue
- awk '
- /^[[:space:]]*#/ || /^[[:space:]]*$/ { next }
- $1 == "repo" && NF >= 3 {
- printf "repo %s %s", $2, $3
- if (NF >= 4) printf " %s", $4
- printf "\n"
+ [ "$sps_repo_file" != "$sps_repo_previous" ] || continue
+ if [ "$sps_repo_file" = "$SPS_CONFIG" ]; then
+ sps_repo_main=1
+ else
+ sps_repo_main=0
+ fi
+ awk -v main_config="$sps_repo_main" '
+ {
+ line = $0
+ sub(/\r$/, "", line)
+ probe = line
+ sub(/^[ \t]+/, "", probe)
+ if (probe == "" || probe ~ /^#/)
+ next
+ split(probe, field, /[ \t]+/)
+ if (!main_config || field[1] == "git" ||
+ field[1] == "dir" || field[1] == "repo")
+ print line
}
- ' "$sps_repo_file"
+ ' "$sps_repo_file" || return $?
+ sps_repo_previous=$sps_repo_file
done
}
@@ -272,6 +453,7 @@ sps_load_config()
[ "${SPS_DB+x}" = x ] && sps_env_db=1 || sps_env_db=0
[ "${SPS_CACHE+x}" = x ] && sps_env_cache=1 || sps_env_cache=0
[ "${SPS_BUILD+x}" = x ] && sps_env_build=1 || sps_env_build=0
+ [ "${SPS_REPO_ROOT+x}" = x ] && sps_env_repo_root=1 || sps_env_repo_root=0
[ "${SPS_MAKEJOBS+x}" = x ] && sps_env_makejobs=1 || sps_env_makejobs=0
[ "${SPS_COMPRESSION+x}" = x ] && sps_env_compression=1 || sps_env_compression=0
[ "${SPS_ARCH+x}" = x ] && sps_env_arch=1 || sps_env_arch=0
@@ -295,6 +477,7 @@ sps_load_config()
sps_cfg_db=/var/lib/sps
sps_cfg_cache=/var/cache/sps
sps_cfg_build=/var/tmp/sps
+ sps_cfg_repo_root=/usr/src/sps
sps_cfg_makejobs=1
sps_cfg_compression=auto
sps_cfg_arch=$(uname -m 2>/dev/null || printf '%s' unknown)
@@ -308,11 +491,12 @@ sps_load_config()
db) sps_cfg_db=$sps_value ;;
cache) sps_cfg_cache=$sps_value ;;
build) sps_cfg_build=$sps_value ;;
+ repo_root) sps_cfg_repo_root=$sps_value ;;
makejobs) sps_cfg_makejobs=$sps_value ;;
compression) sps_cfg_compression=$sps_value ;;
arch) sps_cfg_arch=$sps_value ;;
preserve) sps_cfg_preserve=$sps_value ;;
- repo) : ;;
+ repo|git|dir) : ;;
*) sps_warn "ignoring unknown configuration key '$sps_key' in $SPS_CONFIG" ;;
esac
done < "$SPS_CONFIG"
@@ -326,6 +510,7 @@ sps_load_config()
[ "$sps_env_db" -eq 1 ] || SPS_DB=$(sps_root_path "$sps_cfg_db")
[ "$sps_env_cache" -eq 1 ] || SPS_CACHE=$(sps_root_path "$sps_cfg_cache")
[ "$sps_env_build" -eq 1 ] || SPS_BUILD=$(sps_root_path "$sps_cfg_build")
+ [ "$sps_env_repo_root" -eq 1 ] || SPS_REPO_ROOT=$(sps_root_path "$sps_cfg_repo_root")
[ "$sps_env_makejobs" -eq 1 ] || SPS_MAKEJOBS=$sps_cfg_makejobs
[ "$sps_env_compression" -eq 1 ] || SPS_COMPRESSION=$sps_cfg_compression
[ "$sps_env_arch" -eq 1 ] || SPS_ARCH=$sps_cfg_arch
@@ -341,12 +526,17 @@ sps_load_config()
''|*[!A-Za-z0-9._+-]*|[!A-Za-z0-9]*)
sps_die "$SPS_EX_USAGE" "invalid architecture '$SPS_ARCH'" ;;
esac
+ case $SPS_REPO_ROOT in
+ /*) ;;
+ *) sps_die "$SPS_EX_USAGE" "SPS_REPO_ROOT must be an absolute path: $SPS_REPO_ROOT" ;;
+ esac
+ [ "$SPS_REPO_ROOT" = / ] || SPS_REPO_ROOT=${SPS_REPO_ROOT%/}
case $SPS_PRESERVE in
'') sps_die "$SPS_EX_USAGE" "preserve may not be empty" ;;
*" "*|*" "*|*"
"*) sps_die "$SPS_EX_USAGE" "preserve must be a comma-separated path-prefix list" ;;
esac
- export SPS_ROOT SPS_DB SPS_CACHE SPS_BUILD SPS_CONFIG SPS_REPOS_CONFIG
+ export SPS_ROOT SPS_DB SPS_CACHE SPS_BUILD SPS_REPO_ROOT SPS_CONFIG SPS_REPOS_CONFIG
export SPS_MAKEJOBS SPS_COMPRESSION SPS_ARCH SPS_PRESERVE
}
diff --git a/lib/config.awk b/lib/config.awk
index 7f0918f..e718ec8 100644
--- a/lib/config.awk
+++ b/lib/config.awk
@@ -1,19 +1,23 @@
-# Read SPS config into caller-provided arrays. Repo lines are kept separately
-# because there can be more than one.
+# Read SPS config into caller-provided arrays. Repository declarations are kept
+# separately because there can be more than one. `repo` is the compatibility
+# spelling of `dir`.
-function config_read(path, values, repo_name, repo_path, repo_priority,
- line, fields, n, key, rest, count) {
+function config_read(path, values, repo_kind, repo_name, repo_source,
+ repo_priority, line, fields, n, key, rest, count, kind) {
count = 0
while ((getline line < path) > 0) {
line = sps_trim(line)
if (line == "" || line ~ /^#/) continue
n = split(line, fields, /[[:space:]]+/)
key = fields[1]
- if (key == "repo") {
- if (n < 3 || n > 4) sps_error(path ": malformed repo declaration")
+ if (key == "git" || key == "dir" || key == "repo") {
+ if (n < 3 || n > 4)
+ sps_error(path ": malformed repository declaration")
+ kind = key == "repo" ? "dir" : key
count++
+ repo_kind[count] = kind
repo_name[count] = fields[2]
- repo_path[count] = fields[3]
+ repo_source[count] = fields[3]
repo_priority[count] = n == 4 ? fields[4] : 0
continue
}
@@ -24,4 +28,3 @@ function config_read(path, values, repo_name, repo_path, repo_priority,
close(path)
return count
}
-
diff --git a/lib/deps.awk b/lib/deps.awk
index 275c7a4..c1eefa4 100644
--- a/lib/deps.awk
+++ b/lib/deps.awk
@@ -19,6 +19,22 @@ function deps_error(message) {
deps_failed = 1
}
+function deps_sort_names(values, count, gap, i, j, saved) {
+ gap = int(count / 2)
+ while (gap > 0) {
+ for (i = gap + 1; i <= count; i++) {
+ saved = values[i]
+ j = i
+ while (j > gap && values[j - gap] > saved) {
+ values[j] = values[j - gap]
+ j -= gap
+ }
+ values[j] = saved
+ }
+ gap = int(gap / 2)
+ }
+}
+
function deps_dependency_name(specification, parent, position, operator,
name, wanted) {
specification = deps_trim(specification)
@@ -164,8 +180,18 @@ BEGIN {
next
}
- if (!($1 in deps_selected_priority) ||
- ($9 + 0) > deps_selected_priority[$1]) {
+ if (!($1 in deps_selected_priority)) {
+ deps_name_count++
+ deps_names[deps_name_count] = $1
+ deps_selected_priority[$1] = $9 + 0
+ deps_version[$1] = $2
+ deps_release[$1] = $3
+ deps_arch[$1] = $4
+ deps_runtime[$1] = $5
+ deps_build[$1] = $6
+ deps_recipe[$1] = $10
+ deps_recipe_sha256[$1] = $12
+ } else if (($9 + 0) > deps_selected_priority[$1]) {
deps_selected_priority[$1] = $9 + 0
deps_version[$1] = $2
deps_release[$1] = $3
@@ -180,6 +206,17 @@ BEGIN {
END {
if (deps_failed)
exit 5
+ if (action == "validate") {
+ deps_sort_names(deps_names, deps_name_count)
+ for (deps_i = 1; deps_i <= deps_name_count; deps_i++) {
+ deps_visit(deps_names[deps_i], "")
+ if (deps_failed)
+ break
+ }
+ if (deps_failed)
+ exit 5
+ exit 0
+ }
if (roots == "") {
deps_error("no package specified")
exit 2
diff --git a/lib/index.awk b/lib/index.awk
index 36ecd92..34e16cb 100644
--- a/lib/index.awk
+++ b/lib/index.awk
@@ -48,7 +48,7 @@ function index_print_show(name) {
? "-" : index_output_fields[6])
print "optional dependencies: " (index_output_fields[7] == "" \
? "-" : index_output_fields[7])
- print "recipe sha256: " index_output_fields[12]
+ print "definition sha256: " index_output_fields[12]
}
BEGIN {
@@ -75,6 +75,15 @@ BEGIN {
next
}
+ index_repository_key = $8 SUBSEP $1
+ if (index_repository_key in index_repository_recipe) {
+ index_error(FILENAME ":" FNR ": duplicate package '" $1 \
+ "' in repository '" $8 "' (also " \
+ index_repository_recipe[index_repository_key] ")")
+ } else {
+ index_repository_recipe[index_repository_key] = $10
+ }
+
index_records++
index_record_line[index_records] = $0
index_record_name[index_records] = $1
diff --git a/lib/recipe.awk b/lib/recipe.awk
index 75c2b79..e3e1a1d 100644
--- a/lib/recipe.awk
+++ b/lib/recipe.awk
@@ -117,9 +117,15 @@ function recipe_process(line, key, value, separator) {
recipe_set_once(key, value)
} else if (key == "depend" || key == "builddep" || key == "optional") {
recipe_append_dependency(key, value)
- } else if (key == "source" || key == "hash" || key == "prepare" ||
- key == "configure" || key == "build" || key == "install") {
- # mkpkg uses these later; the indexer only records that the fields are valid.
+ } else if (key == "source") {
+ recipe_source_count++
+ } else if (key == "hash") {
+ recipe_hash_count++
+ recipe_hash[recipe_hash_count] = value
+ } else if (key == "install") {
+ recipe_install_count++
+ } else if (key == "prepare" || key == "configure" || key == "build") {
+ # mkpkg executes phase records later; indexing only validates their syntax.
} else {
recipe_error("unknown field '" key "'")
}
@@ -155,6 +161,10 @@ END {
recipe_error("missing required 'version' field")
if (!recipe_seen["release"])
recipe_error("missing required 'release' field")
+ if (!recipe_install_count)
+ recipe_error("missing required 'install' command")
+ if (recipe_source_count != recipe_hash_count)
+ recipe_error("each source must have one corresponding hash")
if (!recipe_seen["arch"]) {
if (recipe_default_arch == "") {
"uname -m" | getline recipe_default_arch
@@ -172,6 +182,16 @@ END {
recipe_value["builddep"] = recipe_expand(recipe_value["builddep"])
recipe_value["optional"] = recipe_expand(recipe_value["optional"])
+ for (recipe_i = 1; recipe_i <= recipe_hash_count; recipe_i++) {
+ recipe_hash_value = recipe_expand(recipe_hash[recipe_i])
+ recipe_hash_hex = substr(recipe_hash_value, 8)
+ if (substr(recipe_hash_value, 1, 7) != "sha256:" ||
+ length(recipe_hash_hex) != 64 ||
+ recipe_hash_hex !~ /^[0123456789abcdefABCDEF]+$/)
+ recipe_error("invalid source hash '" recipe_hash_value \
+ "' (expected sha256:<64 hex digits>)")
+ }
+
if (recipe_value["name"] !~ /^[A-Za-z0-9][A-Za-z0-9+_.-]*$/)
recipe_error("invalid package name '" recipe_value["name"] "'")
if (recipe_value["version"] !~ \
diff --git a/lib/relations.awk b/lib/relations.awk
index b2f91b0..e2d27dc 100644
--- a/lib/relations.awk
+++ b/lib/relations.awk
@@ -15,12 +15,17 @@ function rel_dep_name(spec, p) {
return spec
}
-function rel_add_edges(pkg, list, n, a, i, d) {
+function rel_add_edges(pkg, list, n, a, i, d, key) {
if (list == "") return
n = split(list, a, /,/)
for (i = 1; i <= n; i++) {
d = rel_dep_name(a[i])
- if (d != "") edge[pkg SUBSEP d] = 1
+ key = pkg SUBSEP d
+ if (d != "" && !(key in edge)) {
+ edge[key] = 1
+ child_count[pkg]++
+ child[pkg SUBSEP child_count[pkg]] = d
+ }
}
}
@@ -36,13 +41,11 @@ function rel_sort(a, n, gap,i,j,x) {
}
}
-function rel_find(cur, target, k,p,d) {
+function rel_find(cur, target, i,d) {
if (cur == target) return 1
seen[cur]=1
- for (k in edge) {
- split(k,p,SUBSEP)
- if (p[1] != cur) continue
- d=p[2]
+ for (i = 1; i <= child_count[cur]; i++) {
+ d=child[cur SUBSEP i]
if (seen[d]) continue
parent[d]=cur
if (rel_find(d,target)) return 1
diff --git a/lib/repository.awk b/lib/repository.awk
index f898766..9f943ad 100644
--- a/lib/repository.awk
+++ b/lib/repository.awk
@@ -1,4 +1,12 @@
-# Parse and print repository configuration. Input is: repo NAME PATH [PRIORITY]
+# Parse and print repository configuration.
+#
+# Input forms:
+# git NAME SOURCE [PRIORITY]
+# dir NAME ABSOLUTE_PATH [PRIORITY]
+# repo NAME ABSOLUTE_PATH [PRIORITY] (compatibility alias for dir)
+#
+# Normalized records are tab-separated:
+# KIND NAME SOURCE CHECKOUT PRIORITY ORDER
function repository_trim(value) {
sub(/^[ \t]+/, "", value)
@@ -27,15 +35,19 @@ function repository_error(message) {
repository_count_fields = split(repository_line, repository_fields,
/[ \t]+/)
+ repository_kind = repository_fields[1]
+ if (repository_kind == "repo")
+ repository_kind = "dir"
if (repository_count_fields < 3 || repository_count_fields > 4 ||
- repository_fields[1] != "repo") {
+ (repository_kind != "git" && repository_kind != "dir")) {
repository_error(FILENAME ":" FNR \
- ": expected 'repo NAME PATH [PRIORITY]'")
+ ": expected 'git NAME SOURCE [PRIORITY]' or " \
+ "'dir NAME ABSOLUTE_PATH [PRIORITY]'")
next
}
repository_name = repository_fields[2]
- repository_path = repository_fields[3]
+ repository_source = repository_fields[3]
repository_priority = repository_count_fields == 4 \
? repository_fields[4] : 0
@@ -44,8 +56,26 @@ function repository_error(message) {
repository_name "'")
next
}
- if (repository_path == "" || repository_path ~ /[[:cntrl:]]/) {
- repository_error(FILENAME ":" FNR ": invalid path for repository '" \
+ if (repository_source == "" || repository_source ~ /[[:cntrl:]]/) {
+ repository_error(FILENAME ":" FNR ": invalid source for repository '" \
+ repository_name "'")
+ next
+ }
+ if (repository_kind == "dir" && repository_source !~ /^\//) {
+ repository_error(FILENAME ":" FNR ": directory repository '" \
+ repository_name "' requires an absolute path")
+ next
+ }
+ if (repository_kind == "git" && substr(repository_source, 1, 1) == "-") {
+ repository_error(FILENAME ":" FNR ": invalid Git source for repository '" \
+ repository_name "'")
+ next
+ }
+ repository_source_lower = tolower(repository_source)
+ if (repository_kind == "git" &&
+ repository_source_lower ~ /^https?:\/\/[^\/]*@/) {
+ repository_error(FILENAME ":" FNR \
+ ": credential-bearing HTTP(S) Git source is not permitted for repository '" \
repository_name "'")
next
}
@@ -54,17 +84,39 @@ function repository_error(message) {
repository_priority "'")
next
}
+ repository_priority_number = repository_priority + 0
+ if (repository_priority_number < -2147483648 ||
+ repository_priority_number > 2147483647) {
+ repository_error(FILENAME ":" FNR ": priority out of range '" \
+ repository_priority "'")
+ next
+ }
if (repository_seen[repository_name]) {
repository_error(FILENAME ":" FNR ": duplicate repository '" \
repository_name "'")
next
}
+ if (repository_kind == "git") {
+ if (repo_root !~ /^\// || repo_root ~ /[[:cntrl:]]/) {
+ repository_error("invalid Git repository checkout root '" \
+ repo_root "'")
+ next
+ }
+ repository_checkout = repo_root == "/" \
+ ? "/" repository_name \
+ : repo_root "/" repository_name
+ } else {
+ repository_checkout = repository_source
+ }
+
repository_seen[repository_name] = 1
repository_total++
+ repository_kinds[repository_total] = repository_kind
repository_names[repository_total] = repository_name
- repository_paths[repository_total] = repository_path
- repository_priorities[repository_total] = repository_priority
+ repository_sources[repository_total] = repository_source
+ repository_checkouts[repository_total] = repository_checkout
+ repository_priorities[repository_total] = repository_priority_number
}
END {
@@ -75,16 +127,23 @@ END {
if (action == "normalize" || action == "raw") {
for (repository_i = 1; repository_i <= repository_total;
repository_i++)
- print repository_names[repository_i],
- repository_paths[repository_i],
+ print repository_kinds[repository_i],
+ repository_names[repository_i],
+ repository_sources[repository_i],
+ repository_checkouts[repository_i],
repository_priorities[repository_i], repository_i
} else if (action == "list" || action == "") {
for (repository_i = 1; repository_i <= repository_total;
- repository_i++)
- printf "%s\tpriority %s\t%s\n",
- repository_names[repository_i],
+ repository_i++) {
+ repository_kind = repository_kinds[repository_i]
+ printf "%s\t%s\tpriority %s\t%s",
+ repository_names[repository_i], repository_kind,
repository_priorities[repository_i],
- repository_paths[repository_i]
+ repository_sources[repository_i]
+ if (repository_kind == "git")
+ printf "\tcheckout %s", repository_checkouts[repository_i]
+ printf "\n"
+ }
} else if (action == "status" || action == "status_raw") {
repository_index_state = "missing"
if (index_file != "") {
@@ -105,23 +164,25 @@ END {
repository_i++) {
repository_name = repository_names[repository_i]
if (action == "status_raw")
- print repository_name,
- repository_priorities[repository_i],
- repository_paths[repository_i],
+ print repository_kinds[repository_i], repository_name,
+ repository_sources[repository_i],
+ repository_checkouts[repository_i],
+ repository_priorities[repository_i], repository_i,
(repository_index_state == "present" \
? repository_package_count[repository_name] + 0 \
: "not-indexed")
else if (repository_index_state == "present")
- printf "%s: %d packages (priority %s, %s)\n",
+ printf "%s: %d packages (%s, priority %s, %s)\n",
repository_name,
repository_package_count[repository_name] + 0,
+ repository_kinds[repository_i],
repository_priorities[repository_i],
- repository_paths[repository_i]
+ repository_checkouts[repository_i]
else
- printf "%s: not indexed (priority %s, %s)\n",
- repository_name,
+ printf "%s: not indexed (%s, priority %s, %s)\n",
+ repository_name, repository_kinds[repository_i],
repository_priorities[repository_i],
- repository_paths[repository_i]
+ repository_checkouts[repository_i]
}
} else {
repository_error("unknown repository action '" action "'")
diff --git a/man/mkpkg.1 b/man/mkpkg.1
index e918228..fc5a9b0 100644
--- a/man/mkpkg.1
+++ b/man/mkpkg.1
@@ -40,6 +40,9 @@ Required keys are
.BR version ,
and
.BR release .
+A recipe must also contain at least one
+.B install
+record, and installation must produce a non-empty staged payload.
Common repeatable keys include
.BR source ,
.BR hash ,
@@ -54,11 +57,49 @@ and
.BR install .
Metadata parsing never executes these phase records.
.PP
+Each
+.B source
+must have one corresponding
+.B hash
+in declaration order. The only accepted form is
+.BR sha256:HEX ,
+with exactly 64 hexadecimal digits. There is no checksum-skip value.
+.PP
+The optional
+.B arch
+key defaults to
+.BR SPS_ARCH .
+Use
+.B any
+for an architecture-independent package;
+.B noarch
+is accepted as a compatibility spelling.
+.PP
+A real
+.B files
+or
+.B patches
+directory beside the recipe is copied to
+.B $SRC/files
+or
+.BR $SRC/patches .
+Nothing in these directories is applied or installed automatically.
+.PP
A recipe directory may provide explicit lifecycle scripts in
.B hooks/pre-install,
hooks/post-install, hooks/pre-remove, and hooks/post-remove. No other hook
names are accepted.
.SH ENVIRONMENT
+.B PKG
+is the package staging root;
+.B SRC
+contains acquired sources and support trees;
+.B WORK
+is the extracted source root. Each phase starts there and runs with
+.B /bin/sh
+and options
+.BR -eu .
+.PP
.B SPS_ARCH
provides the default architecture.
.B SPS_MAKEJOBS
@@ -74,8 +115,9 @@ created package artifact, suitable for consumption by
.BR sget (1).
.SH EXIT STATUS
Returns 0 on success, 2 for usage, 3 for missing recipe/source, 4 for invalid
-metadata/package content, 6 for checksum failure, 8 for filesystem failure,
-9 for download failure, and 10 for build failure.
+metadata/package content, 6 for checksum failure, 7 when the output artifact
+already exists, 8 for filesystem failure, 9 for download failure, and 10 for
+build failure.
.SH SEE ALSO
.BR sget (1),
.BR pkin (1),
diff --git a/man/sps.conf.5 b/man/sps.conf.5
index b6ee9a2..0581570 100644
--- a/man/sps.conf.5
+++ b/man/sps.conf.5
@@ -24,6 +24,12 @@ Source, repository-index, and built-package cache; default
Temporary build/staging root; default
.BR /var/tmp/sps .
.TP
+.BI repo_root " path"
+Parent directory for Git repository checkouts; default
+.BR /usr/src/sps .
+The checkout for repository NAME is
+.IR repo_root /NAME.
+.TP
.BI makejobs " count"
Positive per-package build parallelism exported to recipes as
.BR MAKEJOBS .
@@ -59,16 +65,32 @@ to disable the policy.
A declaration has the form:
.PP
.nf
-repo NAME PATH [PRIORITY]
+git NAME SOURCE [PRIORITY]
+dir NAME ABSOLUTE_PATH [PRIORITY]
.fi
.PP
-PATH is a local directory tree. Larger numeric priorities win; configuration
-order breaks equal priorities. Repository transport is outside SPS.
+For
+.BR git ,
+SOURCE is the exact URL or path recorded as the checkout's
+.B origin
+and synchronized by
+.BR src (1).
+For
+.BR dir ,
+ABSOLUTE_PATH is an administrator-managed local recipe tree and SPS performs no
+transport operation. The older keyword
+.B repo
+is accepted as a compatibility alias for
+.BR dir .
+.PP
+PRIORITY is a signed 32-bit integer and defaults to zero. Larger values win;
+configuration order breaks equal priorities. Repository names must be unique.
.SH ENVIRONMENT
.BR SPS_ROOT ,
.BR SPS_DB ,
.BR SPS_CACHE ,
.BR SPS_BUILD ,
+.BR SPS_REPO_ROOT ,
.BR SPS_MAKEJOBS ,
.BR SPS_COMPRESSION ,
.BR SPS_ARCH ,
@@ -85,12 +107,14 @@ root /
db /var/lib/sps
cache /var/cache/sps
build /var/tmp/sps
+repo_root /usr/src/sps
makejobs 8
compression zstd
preserve etc
-repo core /usr/src/core 100
-repo local /usr/src/local 200
+dir local /usr/local/src/sps-local 200
+git core https://github.com/RobertFlexx/sps-core.git 100
+git extra https://github.com/RobertFlexx/sps-extra.git 80
.fi
.SH SEE ALSO
.BR src (1),
diff --git a/man/src.1 b/man/src.1
index 3670a73..a4a1e26 100644
--- a/man/src.1
+++ b/man/src.1
@@ -13,13 +13,15 @@ src status [--raw]
.SH DESCRIPTION
.B src
manages source repository metadata. It never builds or installs packages.
-Configured repositories are local directory trees. Use git, rsync, a release
-archive, or a distribution update script to put repository data on disk.
+It synchronizes configured Git repositories conservatively and indexes those
+checkouts together with configured local directory repositories.
.SH COMMANDS
.TP
.B update
-Scan configured repositories, validate recipes, and atomically replace the
-local package index. Invalid input leaves the previous index in place.
+Clone absent Git repositories, fetch existing clean Git repositories,
+fast-forward safe tracking branches, scan configured trees, validate recipes
+and the selected dependency graph, and atomically replace the local package
+index. Invalid input leaves the previous index in place.
.TP
.BI search " pattern"
Search indexed names and descriptions.
@@ -40,7 +42,23 @@ Show repository paths, priorities, and index state.
selects stable tab-separated output where supported.
.SH SELECTION
Higher numeric repository priority wins. Configuration order breaks equal
-priority, followed by lexical recipe path for deterministic selection.
+priority. Duplicate package names within one repository are rejected.
+.SH GIT SAFETY
+Git checkouts are created below
+.BR $SPS_REPO_ROOT ,
+which defaults to
+.BR /usr/src/sps .
+An existing checkout must be the expected top-level worktree and its
+.B origin
+URL must match the configured source exactly.
+.PP
+.B src update
+refuses tracked or untracked local changes before fetching. It never runs
+.BR "git reset" ,
+.BR "git clean" ,
+or an implicit checkout. A clean tracking branch is fast-forwarded only when
+safe. Detached heads and branches without an origin upstream remain pinned;
+locally ahead branches remain unchanged; divergent branches are refused.
.SH FILES
.TP
.B /etc/sps/sps.conf
@@ -53,8 +71,9 @@ Repository declarations.
Generated local package index.
.SH EXIT STATUS
Returns 0 on success, 2 for usage/configuration, 3 for missing index/package,
-4 for malformed recipes/indexes, 6 for hashing failure, and 8 for permission
-failure.
+4 for malformed recipes/indexes, 5 for an invalid dependency graph, 6 for
+hashing failure, 7 for a checkout conflict, 8 for permission failure, and 9 for
+a Git network failure.
.SH SEE ALSO
.BR sget (1),
.BR mkpkg (1),