diff --git a/.gitignore b/.gitignore index 011f240..44cc862 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ regression.diffs regression.out /sql/semver--?.??.?.sql /semver-* +/semver_annotations.json /latest-changes.md /semver_binary_copy.bin /.vscode diff --git a/Makefile b/Makefile index 2dd11c6..28187f7 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,8 @@ DISTVERSION = $(shell grep -m 1 '[[:space:]]\{3\}"version":' META.json | \ sed -e 's/[[:space:]]*"version":[[:space:]]*"\([^"]*\)",\{0,1\}/\1/') MODULEDIR = $(EXTENSION) +DESCRIPTION = A Postgres data type for the Semantic Version format with support for btree and hash indexing. +REPO_URL = https://github.com/theory/pg-semver DATA = $(wildcard sql/*--*.sql) DOCS = $(wildcard doc/*.mmd) TESTS = $(wildcard test/sql/*.sql) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b1b6d61 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,57 @@ +version: "3.9" +name: trunking + +# Start a Zot service for pushing and pulling OCI images, and a +# pgxn/pgxn-tools image for building and testing Linux/AMD binaries. +# +# docker compose up -d +# docker compose exec linux bash +# make clean && make && make trunk +# +# On macOS: +# make clean && make && make trunk +# ./push_trunk localhost:5000/theory/semver semver-0.32.1+pg16-darwin-23.5.0-arm64 semver-0.32.1+pg16-linux-amd64 +# clean -dfx --exclude=.vscode +# find "$(pg_config --sharedir)" "$(pg_config --pkglibdir)" "$(pg_config --docdir)" -name '*semver*' -exec rm -rf "{}" \; +# +# ./install_trunk localhost:5000/theory/semver:v1 +# find "$(pg_config --sharedir)" "$(pg_config --pkglibdir)" "$(pg_config --docdir)" -name '*semver*' +# +# Back on Linux +# ./install_trunk zot:5000/theory/semver:v1 +# find "$(pg_config --sharedir)" "$(pg_config --pkglibdir)" "$(pg_config --docdir)" -name '*semver*' + +# Name the network for all of the services to join. +networks: + default: + name: pgxnnet + +services: + zot: + image: ghcr.io/project-zot/zot-linux-arm64:latest + container_name: zot + ports: + - 5000:5000 + hostname: zot + + linux: + image: pgxn/pgxn-tools + platform: "linux/amd64" + container_name: linux + hostname: linux + working_dir: /work + volumes: + - ".:/work" + # Install oras, then start Postgres 16 and install rsync and jq + entrypoint: + - "/bin/sh" + - -ecx + - | + cd /tmp + curl -LO "https://github.com/oras-project/oras/releases/download/v1.2.0/oras_1.2.0_linux_amd64.tar.gz" + mkdir -p oras-install/ + tar -zxf oras_1.2.0_*.tar.gz -C oras-install/ + sudo mv oras-install/oras /usr/local/bin/ + rm -rf oras_1.2.0_*.tar.gz oras-install/ + pg-start 16 rsync jq + tail -f /dev/null diff --git a/install_trunk b/install_trunk index c268bc0..97c6abd 100755 --- a/install_trunk +++ b/install_trunk @@ -22,18 +22,27 @@ install_trunk() { fi # Determine the platform. - local my_os my_osv my_arch + local my_os my_osv my_arch platform my_os=$(uname | tr '[:upper:]' '[:lower:]') my_osv=$(uname -r) my_arch="$(uname -m)" if [ "$my_arch" == "x86_64" ]; then my_arch=amd64; fi + if [ "$my_os" == "darwin" ]; then + platform="$my_os/$my_arch:$my_osv" + else + platform="$my_os/$my_arch" + fi + + # Download. + local file + file=$(basename "$(oras pull --no-tty --plain-http --format 'go-template={{(first .files).path}}' --platform "$platform" "$trunk")") # Unpack. - printf "Unpacking %s\n" "$trunk" - tar zxf "$trunk" + printf "Unpacking %s\n" "$file" + tar zxf "$file" # Go into the directory - local dir="${trunk%.*}" + local dir="${file%.*}" cd "$dir" || exit # Verify the checksums. @@ -61,7 +70,7 @@ install_trunk() { exit 1 fi - printf "Verifying compatibility with %s/%s:%s\n" "$my_os" "$my_arch" "$my_osv" + printf "Verifying compatibility with %s\n" "$platform" local pkg_os pkg_os=$(jq -r .platform.os trunk.json) if [ "$pkg_os" != "any" ]; then diff --git a/push_trunk b/push_trunk new file mode 100755 index 0000000..b36d574 --- /dev/null +++ b/push_trunk @@ -0,0 +1,114 @@ +#!/bin/bash + +# POC for publishing Trunk packages to an OCI repository with an image index +# to allow pulling a platform-specific binary. Requires: +# +# * oras +# * jq +# * zot: docker run -d -p 5000:5000 --name oras-quickstart ghcr.io/project-zot/zot-linux-arm64:latest +# +# Inspired by the Homebrew implementation of the pattern as referenced in +# https://github.com/oras-project/oras/issues/237, and with thanks to the +# denizens of the #oras and #zot channels on the CNCF Slack. + +trap 'exit' ERR +set -E + +# OCI types to create. +ARTIFACT_TYPE=application/vnd.pgxn.trunk.layer.v1 +MEDIA_TYPE=application/vnd.oci.image.layer.v1.tar+gzip +CONFIG_TYPE=application/vnd.oci.image.config.v1+json +OCI_DIR=oci_dir +INDEX_FILE=image_index.json + +push_image() { + # Push the image into the OCI layout directory $OCI_DIR. + local trunk=$1 + oras push --no-tty \ + --oci-layout "$OCI_DIR" \ + --artifact-type "${ARTIFACT_TYPE}" \ + --config "${trunk}_config.json":"$CONFIG_TYPE" \ + --format go-template='{{.digest}}' \ + --annotation-file "${trunk}_annotations.json" \ + "$trunk.trunk":"$MEDIA_TYPE" +} + +make_manifest() { + local trunk=$1 + local digest=$2 + local anno platform + + # Extract just the pgxn.trunk annotations. + anno=$(jq -c \ + '.["$manifest"] | with_entries(select(.key | startswith("org.pgxn.trunk.")))' \ + "${trunk}_annotations.json" + ) + + # Extract the platform config. + platform=$(jq \ + 'pick(.os, .["os.version"], .architecture)| with_entries(select(.value |. !=null and . != ""))' \ + "$trunk"_config.json + ) + + # Create and return the image manifest. + oras manifest fetch --oci-layout "$OCI_DIR@${digest}" --descriptor \ + | jq --argjson anno "$anno" --argjson platform "$platform" \ + '{ + mediaType: .mediaType, + size: .size, + digest: .digest, + platform: $platform, + annotations: $anno + }' +} + +write_index() { + darwin_manifest=$1 + linux_manifest=$2 + + # Build the image index with the two manifests. + jq -n --argjson linux "$linux_manifest" \ + --argjson darwin "$darwin_manifest" \ + --argjson annotations "$(cat semver_annotations.json)" \ + '{ + schemaVersion: 2, + mediaType: "application/vnd.oci.image.index.v1+json", + manifests: [$linux, $darwin], + annotations: $annotations + }' > "$INDEX_FILE" +} + +push_trunk() { + # Only testing for Darwin and Linux rn. + local repo=$1 + local darwin_trunk=$2 + local linux_trunk=$3 + + if [ -z "$repo" ] || [ -z "$darwin_trunk" ] || [ -z "$linux_trunk" ]; then + printf "Usage:\n\n %s REPO DARWIN.trunk LINUX.trunk\n" "$0" + exit 1 + fi + + # Push the images and grab the resulting digests. + darwin_digest=$(push_image "$darwin_trunk") + linux_digest=$(push_image "$linux_trunk") + + # Create the image manifests. + darwin_manifest=$(make_manifest "$darwin_trunk" "$darwin_digest") + linux_manifest=$(make_manifest "$linux_trunk" "$linux_digest") + + # Write out and push the image index. + write_index "$darwin_manifest" "$linux_manifest" + oras manifest push --oci-layout ./"$OCI_DIR":image-index "$INDEX_FILE" + + # Push everything from the local layout to the remote registry. + oras cp --from-oci-layout ./"$OCI_DIR":image-index --to-plain-http "${repo}:v1" + + # View the remote image index manifest. + oras manifest get --plain-http "${repo}:v1" | jq + + # Cleanup. + rm -rf "$OCI_DIR" "$INDEX_FILE" +} + +push_trunk "$@" diff --git a/trunk.mk b/trunk.mk index 4a23894..b2ec09c 100644 --- a/trunk.mk +++ b/trunk.mk @@ -1,10 +1,16 @@ # Trunk packaging. To use, create a standard PGXS Makefile for your extension. -# The only extra variables are DISTVERSION, LICENSE, and LANGUAGE. +# The only extra variables are DISTVERSION, TITLE, DESCRIPTION, LICENSE, +# LANGUAGE, VENDOR, URL, and REPO_URL. # # ``` make # DISTVERSION = 1.2.1 +# TITLE = Bike +# DESCRIPTION = A bicycle inside your database. # LICENSE = mit # LANGUAGE = c +# VENDOR = PGXN +# URL = https://pgxn.org/dist/bike +# REPO_URL = https://github.com/pgxn/bike # # EXTENSION = bike # MODULEDIR = $(EXTENSION) @@ -38,6 +44,9 @@ LAUNGUAGE ?= c LICENSE ?= PostgreSQL +TITLE ?= $(EXTENSION) +VENDOR ?= PGXN +URL ?= $(REPO_URL) pkg_arch := $(shell uname -m) ifeq ($(pkg_arch),x86_64) @@ -64,7 +73,52 @@ pkg_info_files ?= $(wildcard README* readme* Readme* LICENSE* license* License* EXTRA_CLEAN += $(EXTENSION)-$(DISTVERSION)+*/ # Phony target to create the trunk and OCI JSON files. -trunk: $(pkg).trunk +trunk: $(pkg).trunk $(pkg)_config.json $(pkg)_annotations.json $(EXTENSION)_annotations.json + +# Use jq to create the OCI image index annoations. +# https://github.com/opencontainers/image-spec/blob/main/annotations.md +$(EXTENSION)_annotations.json: + jq -n \ + --arg org.opencontainers.image.created "$$(date +%Y-%m-%dT%TZ)" \ + --arg org.opencontainers.image.licenses "$(LICENSE)" \ + --arg org.opencontainers.image.title "$(TITLE)" \ + --arg org.opencontainers.image.description "$(DESCRIPTION)" \ + --arg org.opencontainers.image.source "$(REPO_URL)" \ + --arg org.opencontainers.image.vendor "$(VENDOR)" \ + --arg org.opencontainers.image.ref.name "$(DISTVERSION)" \ + --arg org.opencontainers.image.version "$(DISTVERSION)" \ + --arg org.opencontainers.image.url "$(URL)" \ + '$$ARGS.named | with_entries(select(.value |. !=null and . != ""))' > $@ + +# Use jq to create the OCI image configuration. +$(pkg)_config.json: + @jq -n \ + --arg os "$(PORTNAME)" \ + --arg os.version "$(pkg_os_ver)" \ + --arg architecture "$(pkg_arch)" \ + --arg created "$$(date +%Y-%m-%dT%TZ)" \ + '$$ARGS.named | with_entries(select(.value |. !=null and . != ""))' > $@ + +# Use jq to create the OCI image manifest annotations. +$(pkg)_annotations.json: $(pkg).trunk $(pkg)_config.json + @anno=$$(jq -n \ + --arg org.opencontainers.image.created "$$(date +%Y-%m-%dT%TZ)" \ + --arg org.opencontainers.image.title "$(pkg).trunk" \ + --arg org.opencontainers.image.licenses "$(LICENSE)" \ + --arg org.opencontainers.image.description "$(DESCRIPTION)" \ + --arg org.opencontainers.image.source "$(REPO_URL)" \ + --arg org.opencontainers.image.vendor "$(VENDOR)" \ + --arg org.opencontainers.image.ref.name "$(DISTVERSION)" \ + --arg org.opencontainers.image.version "$(DISTVERSION)" \ + --arg org.opencontainers.image.url "$(URL)" \ + --arg org.pgxn.trunk.pg.version "$(VERSION)" \ + --arg org.pgxn.trunk.pg.major "$(MAJORVERSION)" \ + --arg org.pgxn.trunk.pg.version_num "$(VERSION_NUM)" \ + --arg org.pgxn.trunk.version "0.1.0" \ + '$$ARGS.named | with_entries(select(.value |. !=null and . != ""))' \ + ) && jq -n \ + --argjson '$$manifest' "$$anno" \ + '$$ARGS.named' > $@ $(pkg).trunk: package tar zcvf $@ $(pkg_dir)