Initial opam repository with opam-publish tool

Changed files
+405
bin
packages
+14
.gitignore
··· 1 + # Build artifacts 2 + _build/ 3 + *.tbz 4 + *.tar.gz 5 + *.tar.bz2 6 + 7 + # Editor files 8 + *~ 9 + .*.swp 10 + .*.swo 11 + 12 + # OS files 13 + .DS_Store 14 + Thumbs.db
+15
LICENSE
··· 1 + ISC License 2 + 3 + Copyright (c) 2026 Gabriel Díaz López de la Llave 4 + 5 + Permission to use, copy, modify, and/or distribute this software for any 6 + purpose with or without fee is hereby granted, provided that the above 7 + copyright notice and this permission notice appear in all copies. 8 + 9 + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 14 + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 + PERFORMANCE OF THIS SOFTWARE.
+236
README.md
··· 1 + # Private OPAM Repository 2 + 3 + A private OPAM repository with tooling for publishing OCaml packages. 4 + 5 + ## Repository Structure 6 + 7 + ``` 8 + ├── bin/opam-publish # CLI tool for publishing packages 9 + ├── packages/ # Published package metadata 10 + │ └── <pkg>/ 11 + │ └── <pkg>.<version>/ 12 + │ └── opam 13 + └── repo # OPAM repository marker 14 + ``` 15 + 16 + ## Architecture 17 + 18 + ``` 19 + ┌──────────────┐ opam-publish ┌─────────────┐ 20 + │ Your Package │ ────────────────▶│ S3 Storage │◀── tarballs 21 + └──────────────┘ └─────────────┘ 22 + 23 + 24 + ┌─────────────┐ 25 + │ Cloudflare │◀── public reads 26 + └─────────────┘ 27 + 28 + ┌──────────────┐ opam-publish ┌─────────────┐ 29 + │ Your Package │ ────────────────▶│ This Repo │◀── metadata only 30 + └──────────────┘ └─────────────┘ 31 + ``` 32 + 33 + ## Setup 34 + 35 + ### Requirements 36 + 37 + - `mc` (MinIO client, for S3-compatible uploads) 38 + - `git` (for pushing to this repo) 39 + - `tar`, `sha256sum` (standard Unix tools) 40 + - `opam` (optional, for linting validation) 41 + - SSH access to this repository 42 + 43 + ### Configuration 44 + 45 + **1. Configure mc alias** (stores credentials securely in `~/.mc/`): 46 + 47 + ```bash 48 + mc alias set mys3 https://your-s3-endpoint ACCESS_KEY SECRET_KEY 49 + ``` 50 + 51 + **2. Create opam-publish config** (`~/.config/opam-publish/config`): 52 + 53 + ```bash 54 + mkdir -p ~/.config/opam-publish 55 + chmod 700 ~/.config/opam-publish 56 + 57 + cat > ~/.config/opam-publish/config << 'EOF' 58 + MC_ALIAS=mys3 59 + S3_BUCKET=your-bucket 60 + PUBLIC_URL_BASE=https://packages.yourdomain.com 61 + OPAM_REPO_URL=git@your-git-host:path/to/this/repo 62 + EOF 63 + 64 + chmod 600 ~/.config/opam-publish/config 65 + ``` 66 + 67 + ### Add to PATH 68 + 69 + ```bash 70 + export PATH="$PATH:/path/to/this/repo/bin" 71 + ``` 72 + 73 + Or symlink: 74 + 75 + ```bash 76 + ln -s /path/to/this/repo/bin/opam-publish ~/.local/bin/ 77 + ``` 78 + 79 + ## Publishing a Package 80 + 81 + ### Prerequisites 82 + 83 + Your OCaml project must have: 84 + 85 + 1. A `<package>.opam` file (or generate via `dune build`) 86 + 2. A `version` field in the `.opam` file or `dune-project` 87 + 88 + ### Publish 89 + 90 + From your project root: 91 + 92 + ```bash 93 + opam-publish 94 + ``` 95 + 96 + If multiple `.opam` files exist, specify which one: 97 + 98 + ```bash 99 + opam-publish mypackage 100 + ``` 101 + 102 + ### What Happens 103 + 104 + 1. Validates `.opam` file (required fields, runs `opam lint` if available) 105 + 2. Builds a source tarball (excludes `.git`, `_build`, `_opam`) 106 + 3. Computes SHA256 checksum 107 + 4. Uploads tarball to S3 108 + 5. Clones this repo, adds package metadata with URL + checksum 109 + 6. Pushes commit to this repo 110 + 111 + ### Example Output 112 + 113 + ``` 114 + ==> Validating mylib.opam... 115 + ok 116 + ==> Building mylib.1.0.0.tbz... 117 + 42K 118 + ==> Uploading to S3... 119 + https://packages.example.com/mylib/mylib.1.0.0.tbz 120 + ==> Cloning opam repository... 121 + ==> Pushing to opam repository... 122 + ==> Published mylib 1.0.0 123 + 124 + Done. Users can install with: 125 + opam install mylib 126 + ``` 127 + 128 + ## Using This Repository 129 + 130 + ### Add to OPAM 131 + 132 + ```bash 133 + opam repository add private git+ssh://git@your-git-host/path/to/repo.git 134 + ``` 135 + 136 + Or via HTTPS: 137 + 138 + ```bash 139 + opam repository add private git+https://your-git-host/path/to/repo.git 140 + ``` 141 + 142 + ### Install Packages 143 + 144 + ```bash 145 + opam update 146 + opam install <package> 147 + ``` 148 + 149 + ### Remove Repository 150 + 151 + ```bash 152 + opam repository remove private 153 + ``` 154 + 155 + ## Releasing a New Version 156 + 157 + 1. Update version in `dune-project`: 158 + ```lisp 159 + (version 1.2.0) 160 + ``` 161 + 162 + 2. Regenerate opam file: 163 + ```bash 164 + dune build 165 + ``` 166 + 167 + 3. Commit changes: 168 + ```bash 169 + git add -A 170 + git commit -m "Release 1.2.0" 171 + git tag v1.2.0 172 + git push origin main --tags 173 + ``` 174 + 175 + 4. Publish: 176 + ```bash 177 + opam-publish 178 + ``` 179 + 180 + ## Troubleshooting 181 + 182 + ### "version not found" 183 + 184 + Ensure your `.opam` file has a version field: 185 + 186 + ``` 187 + version: "1.0.0" 188 + ``` 189 + 190 + Or in `dune-project`: 191 + 192 + ```lisp 193 + (version 1.0.0) 194 + ``` 195 + 196 + ### "version already published" 197 + 198 + Each version can only be published once. Bump the version number. 199 + 200 + ### S3 upload fails 201 + 202 + Check mc alias and bucket access: 203 + 204 + ```bash 205 + mc alias list 206 + mc ls mys3/your-bucket 207 + ``` 208 + 209 + ### Git push fails 210 + 211 + Ensure you have SSH access to this repository: 212 + 213 + ```bash 214 + ssh -T git@your-git-host 215 + ``` 216 + 217 + ### "opam lint failed" 218 + 219 + Fix the issues reported by `opam lint`. Common problems: 220 + 221 + ```bash 222 + opam lint mylib.opam 223 + ``` 224 + 225 + Missing fields can be added to `dune-project`: 226 + 227 + ```lisp 228 + (generate_opam_files true) 229 + (name mylib) 230 + (synopsis "Short description") 231 + (authors "Your Name") 232 + (maintainers "your@email.com") 233 + (license MIT) 234 + ``` 235 + 236 + Then regenerate with `dune build`.
+139
bin/opam-publish
··· 1 + #!/usr/bin/env bash 2 + set -euo pipefail 3 + 4 + CONFIG_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/opam-publish/config" 5 + 6 + die() { echo "error: $*" >&2; exit 1; } 7 + 8 + load_config() { 9 + [[ -f "$CONFIG_FILE" ]] || die "config not found: $CONFIG_FILE 10 + Create it with: 11 + mkdir -p ~/.config/opam-publish 12 + cat > ~/.config/opam-publish/config << 'EOF' 13 + MC_ALIAS=mys3 14 + S3_BUCKET=your-bucket 15 + PUBLIC_URL_BASE=https://packages.yourdomain.com 16 + OPAM_REPO_URL=git@your-git-host:your-repo 17 + EOF 18 + 19 + Then configure mc: 20 + mc alias set mys3 https://your-s3-endpoint ACCESS_KEY SECRET_KEY" 21 + source "$CONFIG_FILE" 22 + } 23 + 24 + find_package() { 25 + local opam_files=(*.opam) 26 + [[ ${#opam_files[@]} -eq 0 ]] && die "no .opam file found" 27 + [[ ${#opam_files[@]} -gt 1 ]] && die "multiple .opam files found, specify one: $*" 28 + echo "${opam_files[0]%.opam}" 29 + } 30 + 31 + get_version() { 32 + local pkg="$1" 33 + grep -E '^version:' "${pkg}.opam" 2>/dev/null | head -1 | sed 's/version:[[:space:]]*"\(.*\)"/\1/' || \ 34 + grep -E '^\(version' dune-project 2>/dev/null | head -1 | sed 's/(version[[:space:]]*\(.*\))/\1/' || \ 35 + die "version not found in ${pkg}.opam or dune-project" 36 + } 37 + 38 + validate_package() { 39 + local pkg="$1" version="$2" 40 + echo "==> Validating ${pkg}.opam..." 41 + 42 + local opam_file="${pkg}.opam" 43 + 44 + grep -qE '^opam-version:' "$opam_file" || die "missing opam-version field" 45 + grep -qE '^synopsis:' "$opam_file" || die "missing synopsis field" 46 + grep -qE '^maintainer:|^authors:' "$opam_file" || die "missing maintainer or authors field" 47 + 48 + if command -v opam &>/dev/null; then 49 + local lint_output 50 + if ! lint_output=$(opam lint "$opam_file" 2>&1); then 51 + echo "$lint_output" >&2 52 + die "opam lint failed" 53 + fi 54 + local warnings 55 + warnings=$(echo "$lint_output" | grep -c "warning" || true) 56 + [[ "$warnings" -gt 0 ]] && echo " ${warnings} warning(s) from opam lint" 57 + fi 58 + 59 + echo " ok" 60 + } 61 + 62 + build_tarball() { 63 + local pkg="$1" version="$2" archive="$3" 64 + echo "==> Building ${archive}..." 65 + tar --transform "s,^\.,${pkg}.${version}," \ 66 + --exclude='.git' \ 67 + --exclude='_build' \ 68 + --exclude='_opam' \ 69 + --exclude='.tangled' \ 70 + -cjf "${archive}" . 71 + echo " $(du -h "${archive}" | cut -f1)" 72 + } 73 + 74 + upload_s3() { 75 + local pkg="$1" archive="$2" 76 + local filename 77 + filename=$(basename "${archive}") 78 + echo "==> Uploading to S3..." 79 + mc cp --quiet "${archive}" "${MC_ALIAS}/${S3_BUCKET}/${pkg}/${filename}" 80 + echo " ${PUBLIC_URL_BASE}/${pkg}/${filename}" 81 + } 82 + 83 + publish_opam() { 84 + local pkg="$1" version="$2" archive="$3" checksum="$4" 85 + local tmpdir 86 + tmpdir=$(mktemp -d) 87 + trap "rm -rf ${tmpdir}" EXIT 88 + 89 + echo "==> Cloning opam repository..." 90 + git clone --depth 1 "${OPAM_REPO_URL}" "${tmpdir}/repo" 2>/dev/null 91 + 92 + local pkg_dir="${tmpdir}/repo/packages/${pkg}/${pkg}.${version}" 93 + [[ -d "$pkg_dir" ]] && die "version ${version} already published" 94 + 95 + mkdir -p "${pkg_dir}" 96 + cp "${pkg}.opam" "${pkg_dir}/opam" 97 + 98 + printf '\nurl {\n src: "%s/%s/%s"\n checksum: "sha256=%s"\n}\n' \ 99 + "${PUBLIC_URL_BASE}" "${pkg}" "${archive}" "${checksum}" \ 100 + >> "${pkg_dir}/opam" 101 + 102 + echo "==> Pushing to opam repository..." 103 + cd "${tmpdir}/repo" 104 + git add packages/ 105 + git commit -m "Publish ${pkg} ${version}" --quiet 106 + git push origin main --quiet 107 + 108 + echo "==> Published ${pkg} ${version}" 109 + } 110 + 111 + main() { 112 + load_config 113 + 114 + local pkg="${1:-$(find_package)}" 115 + [[ -f "${pkg}.opam" ]] || die "${pkg}.opam not found" 116 + 117 + local version 118 + version=$(get_version "$pkg") 119 + [[ -n "$version" ]] && [[ "$version" != *'"'* ]] || die "invalid version: $version" 120 + 121 + local archive="${pkg}.${version}.tbz" 122 + 123 + validate_package "$pkg" "$version" 124 + build_tarball "$pkg" "$version" "/tmp/${archive}" 125 + 126 + local checksum 127 + checksum=$(sha256sum "/tmp/${archive}" | cut -d' ' -f1) 128 + 129 + upload_s3 "$pkg" "/tmp/${archive}" 130 + publish_opam "$pkg" "$version" "$archive" "$checksum" 131 + 132 + rm -f "/tmp/${archive}" 133 + 134 + echo "" 135 + echo "Done. Users can install with:" 136 + echo " opam install ${pkg}" 137 + } 138 + 139 + main "$@"
packages/.gitkeep

This is a binary file and will not be displayed.

+1
repo
··· 1 + opam-version: "2.0"