├── docs ├── targets │ ├── npm.md │ ├── spm.md │ ├── sqlpkg.md │ ├── amalgamation.md │ ├── datasette.md │ ├── sqlite-utils.md │ ├── github-releases.md │ └── pip.md ├── .gitignore ├── package.json ├── index.md ├── config.md ├── intro.md └── .vitepress │ └── config.mts ├── sample ├── VERSION ├── .ruby-version ├── wasm32-emscripten │ ├── sqlite3.mjs │ └── sqlite3.wasm ├── tmp.py ├── extra_init.py ├── .gitignore ├── Gemfile ├── test.rb ├── sqlite-sample.h.tmpl ├── sqlite-sample.h ├── test.mjs ├── sqlite-dist.toml ├── sqlite-sample.c ├── Makefile └── build.sh ├── .gitignore ├── src ├── targets │ ├── mod.rs │ ├── manifest.rs │ ├── sqlpkg.rs │ ├── amalgamation.rs │ ├── spm.rs │ ├── gh_releases.rs │ ├── installer_sh.rs │ ├── gem.rs │ ├── pip.rs │ └── npm.rs ├── publish.rs ├── main.rs ├── cli.rs ├── spec.rs └── build.rs ├── NOTES ├── README.md ├── dist-workspace.toml ├── Cargo.toml ├── docs.md ├── .github └── workflows │ ├── site.yml │ └── release.yml └── Cargo.lock /docs/targets/npm.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/targets/spm.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/targets/sqlpkg.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/targets/amalgamation.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/targets/datasette.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/targets/sqlite-utils.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample/VERSION: -------------------------------------------------------------------------------- 1 | 0.0.1-alpha.1 -------------------------------------------------------------------------------- /docs/targets/github-releases.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample/.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2.2 2 | -------------------------------------------------------------------------------- /sample/wasm32-emscripten/sqlite3.mjs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample/wasm32-emscripten/sqlite3.wasm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | .vitepress/dist 2 | .vitepress/cache -------------------------------------------------------------------------------- /sample/tmp.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | print(sys.argv) 4 | -------------------------------------------------------------------------------- /sample/extra_init.py: -------------------------------------------------------------------------------- 1 | def foo(): 2 | return "yoo" 3 | -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | vendor/ 3 | 4 | *.dylib 5 | *.dll 6 | *.so 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | tmp/ 3 | .vscode 4 | 5 | 6 | research/ 7 | node_modules 8 | sample/package*.json 9 | -------------------------------------------------------------------------------- /sample/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 6 | 7 | # gem "rails" 8 | -------------------------------------------------------------------------------- /src/targets/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod gem; 2 | pub mod pip; 3 | pub mod npm; 4 | pub mod sqlpkg; 5 | pub mod spm; 6 | pub mod gh_releases; 7 | pub mod amalgamation; 8 | pub mod installer_sh; 9 | pub mod manifest; -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "vitepress dev", 4 | "build": "vitepress build", 5 | "preview": "vitepress preview" 6 | }, 7 | "devDependencies": { 8 | "vitepress": "^2.0.0-alpha.15" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /sample/test.rb: -------------------------------------------------------------------------------- 1 | require 'sqlite3' 2 | require 'sqlite_sample' 3 | db = SQLite3::Database.new(':memory:') 4 | db.enable_load_extension(true) 5 | SqliteSample.load(db) 6 | db.enable_load_extension(false) 7 | result = db.execute('SELECT sample_version()') 8 | puts result.first.first 9 | 10 | -------------------------------------------------------------------------------- /NOTES: -------------------------------------------------------------------------------- 1 | ``` 2 | gh release upload \ 3 | tmp/github_releases/* \ 4 | tmp/spm/* \ 5 | tmp/sqlpkg/* \ 6 | tmp/checksums.txt \ 7 | tmp/sqlite-dist-manifest.json \ 8 | tmp/install.sh 9 | 10 | npm publish --access public tmp/npm/* 11 | gem push tmp/gem/* 12 | python3 -m twine upload 13 | ``` 14 | 15 | ``` 16 | rm -rf tmp/ && mkdir tmp; time cargo run -- sample/sqlite-dist.toml --input sample/dist/ --output tmp --version 0.0.1 && tree --du -h --dirsfirst tmp/ 17 | ``` 18 | -------------------------------------------------------------------------------- /sample/sqlite-sample.h.tmpl: -------------------------------------------------------------------------------- 1 | #ifndef _SQLITE_SAMPLE_H 2 | #define _SQLITE_SAMPLE_H 3 | 4 | #include "sqlite3ext.h" 5 | 6 | #define SQLITE_SAMPLE_VERSION "v${VERSION}" 7 | #define SQLITE_SAMPLE_DATE "${DATE}" 8 | #define SQLITE_SAMPLE_SOURCE "${SOURCE}" 9 | 10 | #ifdef __cplusplus 11 | extern "C" { 12 | #endif 13 | 14 | int sqlite3_sample_init(sqlite3*, char**, const sqlite3_api_routines*); 15 | 16 | #ifdef __cplusplus 17 | } /* end of the 'extern "C"' block */ 18 | #endif 19 | 20 | #endif /* ifndef _SQLITE_SAMPLE_H */ 21 | -------------------------------------------------------------------------------- /sample/sqlite-sample.h: -------------------------------------------------------------------------------- 1 | #ifndef _SQLITE_SAMPLE_H 2 | #define _SQLITE_SAMPLE_H 3 | 4 | #include "sqlite3ext.h" 5 | 6 | #define SQLITE_SAMPLE_VERSION "v0.0.1-alpha.1" 7 | #define SQLITE_SAMPLE_DATE "2024-02-24T22:49:08Z-0800" 8 | #define SQLITE_SAMPLE_SOURCE "" 9 | 10 | #ifdef __cplusplus 11 | extern "C" { 12 | #endif 13 | 14 | int sqlite3_sample_init(sqlite3*, char**, const sqlite3_api_routines*); 15 | 16 | #ifdef __cplusplus 17 | } /* end of the 'extern "C"' block */ 18 | #endif 19 | 20 | #endif /* ifndef _SQLITE_SAMPLE_H */ 21 | -------------------------------------------------------------------------------- /src/publish.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::PublishCommand; 2 | use std::fmt; 3 | 4 | #[derive(Debug)] 5 | pub enum PublishError { 6 | NotImplemented, 7 | } 8 | 9 | impl fmt::Display for PublishError { 10 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 11 | match self { 12 | PublishError::NotImplemented => write!(f, "Publish not yet implemented"), 13 | } 14 | } 15 | } 16 | 17 | pub fn publish(_args: PublishCommand) -> Result<(), PublishError> { 18 | todo!("Publish command not yet implemented"); 19 | } 20 | -------------------------------------------------------------------------------- /sample/test.mjs: -------------------------------------------------------------------------------- 1 | import { Database } from "jsr:@db/sqlite@0.11"; 2 | import * as sqlite_sample from "sqlite-sample"; 3 | import * as sqlite_hello from "npm:sqlite-hello"; 4 | 5 | const db = new Database(":memory:"); 6 | db.enableLoadExtension = true; 7 | sqlite_sample.load(db); 8 | db.loadExtension(sqlite_hello.getLoadablePath()); 9 | 10 | const version = db.prepare("select hello_version()").value(); 11 | //const version = db.prepare("select sample_version()").value(); 12 | //const version = db.prepare("select sample_version()").pluck().get(); 13 | console.log(version); 14 | -------------------------------------------------------------------------------- /docs/targets/pip.md: -------------------------------------------------------------------------------- 1 | # Package SQLite Extensions for Python with `sqlite-dist` 2 | 3 | 4 | The `pip` target in `sqlite-dist` will generate Python wheels for loadable SQLite extensions. 5 | These wheels can then be uploaded to [PyPi](https://pypi.org/), allowing Python developers to 6 | easily `pip install` your SQLite extension. 7 | 8 | As an example, 9 | 10 | 11 | ```toml 12 | [package] 13 | name = "sqlite-sample" 14 | license = "MIT" 15 | homepage = "https://…" 16 | repo = "https://…" 17 | description = "…" 18 | authors = ["…"] 19 | 20 | [targets] 21 | pip = {} 22 | ``` 23 | -------------------------------------------------------------------------------- /sample/sqlite-dist.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sqlite-sample" 3 | license = "MIT OR Apache" 4 | homepage = "https://alexgarcia.xyz/sqlite-sample" 5 | repo = "https://github.com/asg017/sqlite-sample" 6 | description = "A sample SQLite extension to test sqlite-dist." 7 | authors = ["Alex Garcia"] 8 | 9 | [targets] 10 | github_releases = {} 11 | sqlpkg = {} 12 | spm = {} 13 | amalgamation = {include=["sqlite-sample.c", "sqlite-sample.h"]} 14 | 15 | pip = { extra_init_py = "extra_init.py" } 16 | datasette = {} 17 | sqlite_utils = {} 18 | 19 | npm = {} 20 | 21 | gem = { module_name="SqliteSample" } 22 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "sqlite-dist" 7 | text: "Distribute SQLite extensions with ease" 8 | tagline: Publish pre-compiled SQLite extensions to npm, PyPi, RubyGems, GitHub Releases, and more! 9 | actions: 10 | - theme: brand 11 | text: Introduction 12 | link: /intro 13 | - theme: alt 14 | text: sqlite-dist.toml Reference 15 | link: /config 16 | 17 | #features: 18 | # - title: ... 19 | # details: ... 20 | # - title: ... 21 | # details: ... 22 | # - title: ... 23 | # details: ... 24 | --- 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sqlite-dist 2 | 3 | A CLI tool for packing and distributing pre-compiled SQLite extensions. Can be used to distribute SQLite extensions on: 4 | 5 | - Github releases for general use 6 | - [Pypi](https://pypi.org/) for Python developers (`pip install`) 7 | - [npm](https://npmjs.com/) for JavaScript developers (`npm install`) 8 | - [RubyGems](https://rubygems.org/) for Ruby developers (`gem install`) 9 | - As a [Datasette plugin](https://datasette.io/plugins) (`datasette install`) 10 | - As a [`sqlite-utils` plugin](https://sqlite-utils.datasette.io/) (`sqlite-utils install`) 11 | - As a [sqlpkg](https://github.com/nalgeon/sqlpkg) package (`sqlpkg install`) 12 | 13 | Work-in-progress, not complete yet! Watch this repo for updates. 14 | -------------------------------------------------------------------------------- /dist-workspace.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["cargo:."] 3 | 4 | # Config for 'dist' 5 | [dist] 6 | # The preferred dist version to use in CI (Cargo.toml SemVer syntax) 7 | cargo-dist-version = "0.28.0" 8 | # The installers to generate for each app 9 | installers = ["shell"] 10 | # Target platforms to build apps for (Rust target-triple syntax) 11 | targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] 12 | # CI backends to support 13 | ci = "github" 14 | # Which actions to run on pull requests 15 | pr-run-mode = "plan" 16 | # Path that installers should place binaries in 17 | install-path = "CARGO_HOME" 18 | # Whether to install an updater program 19 | install-updater = false 20 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sqlite-dist" 3 | version = "0.0.1-alpha.19" 4 | edition = "2021" 5 | repository = "https://github.com/asg017/sqlite-dist" 6 | 7 | 8 | [dependencies] 9 | base16ct = {version="0.2.0", features=["alloc"]} 10 | base64 = "0.21.7" 11 | chrono = "0.4.34" 12 | clap = { version = "4.5.1", features = ["derive"] } 13 | flate2 = "1.0.28" 14 | semver = {version="1.0.22", features = ["serde"]} 15 | serde = {version="1.0", features = ["derive"]} 16 | serde_json = "1.0" 17 | sha2 = "0.10.8" 18 | tar = "0.4.40" 19 | thiserror = "1.0.57" 20 | toml = "0.8.10" 21 | ureq = "2.9.6" 22 | zip = "0.6.6" 23 | 24 | [profile.dist] 25 | inherits = "release" 26 | lto = "thin" 27 | 28 | [dev-dependencies] 29 | insta = { version = "1.41.1", features = ["yaml"] } 30 | insta-cmd = "0.6.0" 31 | tempdir = "0.3.7" 32 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod targets; 2 | pub(crate) mod cli; 3 | pub(crate) mod spec; 4 | pub(crate) mod build; 5 | pub(crate) mod publish; 6 | 7 | use build::build; 8 | use publish::publish; 9 | use cli::Command; 10 | 11 | fn main() { 12 | let args = cli::Args::parse_args(); 13 | 14 | let result = match args.command { 15 | Command::Build(build_cmd) => { 16 | build(build_cmd).map_err(|e| format!("Build error: {e}")) 17 | } 18 | Command::Publish(publish_cmd) => { 19 | publish(publish_cmd).map_err(|e| format!("Publish error: {e}")) 20 | } 21 | }; 22 | 23 | match result { 24 | Ok(_) => std::process::exit(0), 25 | Err(error) => { 26 | eprintln!("{error}"); 27 | std::process::exit(1); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /docs/config.md: -------------------------------------------------------------------------------- 1 | # `sqlite-dist.toml` Reference 2 | 3 | The `sqlite-dist.toml` file is used to configure the behavior of `sqlite-dist` when building and distributing SQLite extensions. Below is a reference guide to the various configuration options available. 4 | 5 | ## `[package]` 6 | 7 | The `[package]` section contains metadata about the package being distributed. 8 | 9 | ```toml 10 | # sqlite-dist.toml 11 | 12 | [package] 13 | name = "sqlite-sample" 14 | license = "MIT OR Apache" 15 | homepage = "https://alexgarcia.xyz/sqlite-sample" 16 | repo = "https://github.com/asg017/sqlite-sample" 17 | description = "A sample SQLite extension." 18 | authors = ["Alex Garcia"] 19 | ``` 20 | 21 | 22 | ## `[targets]` 23 | 24 | 25 | ```toml 26 | # sqlite-dist.toml 27 | 28 | [package] 29 | # ... see above 30 | 31 | [targets] 32 | pip = {} 33 | ``` -------------------------------------------------------------------------------- /docs.md: -------------------------------------------------------------------------------- 1 | # sqlite-dist 2 | 3 | 4 | ## Targets 5 | 6 | 7 | `sqlite-dist` can target builds for many different package managers across several programming language. Each platform has their own "blessed" way of using SQLite and packaging native libraries, for `sqlite-dist` handles each of them in its own way. 8 | 9 | Each target is completely optional and can be configured with [`sqlite-dist.toml`](#sqlite-disttoml). 10 | 11 | ### Github Releases 12 | 13 | The `github_releases` target 14 | 15 | 16 | ### PyPi Packages (Python) 17 | 18 | 19 | ### NPM Packages (Node.js/Bun/Deno) 20 | 21 | 22 | ### Gems (Ruby) 23 | 24 | 25 | 26 | ### Datasette 27 | 28 | ### `sqlite-utils` 29 | 30 | ### Amalgammation 31 | 32 | ### `sqlpkg` 33 | 34 | ### `spm` 35 | 36 | 37 | ## Reference 38 | 39 | ### CLI Reference 40 | 41 | 42 | 43 | ### `sqlite-dist.toml` -------------------------------------------------------------------------------- /.github/workflows/site.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Site 2 | on: 3 | workflow_dispatch: {} 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "docs/**" 9 | - ".github/**" 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | pages: write 15 | id-token: write 16 | environment: 17 | name: github-pages 18 | url: ${{ steps.deployment.outputs.page_url }} 19 | steps: 20 | - uses: actions/checkout@v5 21 | - uses: actions/setup-node@v6 22 | with: 23 | cache: npm 24 | cache-dependency-path: docs/package-lock.json 25 | - run: npm ci 26 | working-directory: docs/ 27 | - run: npm run build 28 | working-directory: docs/ 29 | - uses: actions/configure-pages@v5 30 | - uses: actions/upload-pages-artifact@v3 31 | with: 32 | path: docs/.vitepress/dist 33 | - id: deployment 34 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /sample/sqlite-sample.c: -------------------------------------------------------------------------------- 1 | #include "sqlite-sample.h" 2 | 3 | #include "sqlite3ext.h" 4 | SQLITE_EXTENSION_INIT1 5 | 6 | static void sample(sqlite3_context *context, int argc, sqlite3_value **argv) { 7 | sqlite3_result_text( 8 | context, 9 | "yo!", 10 | -1, 11 | SQLITE_STATIC 12 | ); 13 | } 14 | static void sample_version(sqlite3_context *context, int argc, sqlite3_value **argv) { 15 | sqlite3_result_text(context, SQLITE_SAMPLE_VERSION, -1, SQLITE_STATIC); 16 | } 17 | 18 | #ifdef _WIN32 19 | __declspec(dllexport) 20 | #endif 21 | int sqlite3_sample_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi) { 22 | SQLITE_EXTENSION_INIT2(pApi); 23 | int rc = SQLITE_OK; 24 | if(rc == SQLITE_OK) rc = sqlite3_create_function_v2(db, "sample", 0, SQLITE_UTF8 | SQLITE_DETERMINISTIC, 0, sample, 0, 0, 0); 25 | if(rc == SQLITE_OK) rc = sqlite3_create_function_v2(db, "sample_version", 0, SQLITE_UTF8 | SQLITE_DETERMINISTIC, 0, sample_version, 0, 0, 0); 26 | return rc; 27 | } 28 | -------------------------------------------------------------------------------- /src/targets/manifest.rs: -------------------------------------------------------------------------------- 1 | use crate::build::{GeneratedAsset, GeneratedAssetKind}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::io::Result; 4 | use std::path::Path; 5 | 6 | #[derive(Debug, Deserialize, Serialize)] 7 | pub struct ManifestBuildInfo { 8 | sqlite_dist_version: String, 9 | } 10 | 11 | #[derive(Serialize)] 12 | pub struct Manifest<'a> { 13 | build_info: ManifestBuildInfo, 14 | 15 | artifacts: &'a [GeneratedAsset], 16 | } 17 | 18 | pub(crate) fn write_manifest( 19 | manifest_dir: &Path, 20 | generated_assets: &[GeneratedAsset], 21 | ) -> Result { 22 | let manifest = Manifest { 23 | build_info: ManifestBuildInfo { 24 | sqlite_dist_version: "TODO".to_owned(), 25 | }, 26 | artifacts: generated_assets, 27 | }; 28 | let asset = GeneratedAsset::from( 29 | GeneratedAssetKind::Sqlpkg, 30 | &manifest_dir.join("sqlite-dist-manifest.json"), 31 | serde_json::to_string_pretty(&manifest)?.as_bytes(), 32 | )?; 33 | Ok(asset) 34 | } 35 | -------------------------------------------------------------------------------- /sample/Makefile: -------------------------------------------------------------------------------- 1 | ifndef TARGET_LOADABLE 2 | TARGET_LOADABLE=sample0.dylib 3 | endif 4 | 5 | # -lsqlite3 6 | CFLAGS= 7 | 8 | ifdef TARGET 9 | CFLAGS += -target $(TARGET) 10 | endif 11 | 12 | $(TARGET_LOADABLE): sqlite-sample.c 13 | zig cc \ 14 | -fPIC -shared \ 15 | -Wall -Wextra -Wno-unused-parameter -Wno-unused-variable \ 16 | -Ivendor \ 17 | -O3 \ 18 | $(CFLAGS) \ 19 | $< \ 20 | -o $@ 21 | 22 | loadable: $(TARGET_LOADABLE) 23 | 24 | $(TARGET_STATIC): sqlite-sample.c $(prefix) 25 | zig cc \ 26 | -Wall -Wextra -Wno-unused-parameter -Wno-unused-variable \ 27 | -Ivendor $(CFLAGS) -DSQLITE_CORE \ 28 | -O3 -c $< -o tmp.o 29 | zig ar rcs $@ tmp.o 30 | rm tmp.o 31 | 32 | static: $(TARGET_STATIC) 33 | 34 | sqlite-sample.h: sqlite-sample.h.tmpl VERSION 35 | VERSION=$(shell cat VERSION) \ 36 | DATE=$(shell date -r VERSION +'%FT%TZ%z') \ 37 | SOURCE=$(shell git log -n 1 --pretty=format:%H -- VERSION) \ 38 | envsubst < $< > $@ 39 | 40 | $(TARGET_H): sqlite-sample.h 41 | cp $< $@ 42 | 43 | h: $(TARGET_H) 44 | 45 | .PHONY: loadable static h 46 | -------------------------------------------------------------------------------- /docs/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to `sqlite-dist` 2 | 3 | [SQLite extensions](https://sqlite.org/loadext.html) are a powerful way to add custom functions, virtual tables, and virtual filesystems to SQLite. 4 | 5 | *Loadable* SQLite extensions are compiled shared object files (`.so` on Linux, `.dylib` on MacOS, `.dll` on Windows) that can be used in many places — on laptops, servers, phones, watches, embedded devices. Every programming languages has their own SQLite client libraries. So, making sure your SQLite extensions are available to users across all platforms and programming languages is challenging! 6 | 7 | That's where `sqlite-dist` comes in. `sqlite-dist` is a tool meant to be ran during your SQLite extension's release process (ie in Github Actions or other CI/CD workflows). It will package up pre-compiled SQLite extensions for various platforms (ie MacOS ARM, Linux x86_64, Linux ARM, Windows x86_64, etc.) and generate packages for various programming languages. 8 | 9 | Here's an example — say you have created a SQLite extension, and have compiled it for various platforms. 10 | 11 | ## Buildintg -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::path::PathBuf; 3 | 4 | #[derive(Parser, Debug)] 5 | #[command( 6 | name = env!("CARGO_PKG_NAME"), 7 | version = env!("CARGO_PKG_VERSION"), 8 | author = "Alex Garcia", 9 | about = "Package and distribute pre-compiled SQLite extensions" 10 | )] 11 | pub struct Args { 12 | #[command(subcommand)] 13 | pub command: Command, 14 | } 15 | 16 | #[derive(Parser, Debug)] 17 | pub enum Command { 18 | /// Build SQLite distribution packages 19 | Build(BuildCommand), 20 | /// Publish distribution packages 21 | Publish(PublishCommand), 22 | } 23 | 24 | #[derive(Parser, Debug)] 25 | pub struct BuildCommand { 26 | /// Sets the input file 27 | #[arg(value_name = "FILE")] 28 | pub file: PathBuf, 29 | 30 | /// Sets the input directory 31 | #[arg(long, value_name = "INPUT_DIR")] 32 | pub input: Option, 33 | 34 | /// Sets the output directory 35 | #[arg(long, value_name = "OUTPUT_DIR")] 36 | pub output: Option, 37 | 38 | /// Set the version 39 | #[arg(long, value_name = "VERSION", required = true)] 40 | pub set_version: String, 41 | } 42 | 43 | #[derive(Parser, Debug)] 44 | pub struct PublishCommand { 45 | // Placeholder for publish-specific flags 46 | } 47 | 48 | impl Args { 49 | pub fn parse_args() -> Self { 50 | ::parse() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /docs/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | 3 | // https://vitepress.dev/reference/site-config 4 | export default defineConfig({ 5 | title: "sqlite-dist", 6 | description: "A tool for distributing SQLite extensions through npm, pip, gem, Github, andmore!", 7 | base: '/sqlite-dist/', 8 | themeConfig: { 9 | // https://vitepress.dev/reference/default-theme-config 10 | nav: [ 11 | { text: 'Home', link: '/' }, 12 | { text: 'Reference', link: '/config' } 13 | ], 14 | 15 | sidebar: [ 16 | { 17 | //text: 'sqlite-dist', 18 | items: [ 19 | { text: 'Introduction', link: '/intro' }, 20 | { text: 'sqlite-dist.toml Reference', link: '/config' } 21 | ], 22 | }, 23 | { 24 | text: "Targets", 25 | items: [ 26 | { text: 'Node.js (npm)', link: '/targets/npm' }, 27 | { text: 'Python (pip)', link: '/targets/pip' }, 28 | { text: 'Ruby (gem)', link: '/targets/gem' }, 29 | { text: 'GitHub Releases', link: '/targets/github-releases' }, 30 | {text: "Datasette", link: "/targets/datasette" }, 31 | {text: "sqlite-utils", link: "/targets/sqlite-utils" }, 32 | {text: "sqlpkg", link: "/targets/sqlpkg" }, 33 | {text: "SPM", link: "/targets/spm" }, 34 | {text: "Amalgamation", link: "/targets/amalgamation" } 35 | 36 | ] 37 | } 38 | ], 39 | 40 | socialLinks: [ 41 | { icon: 'github', link: 'https://github.com/asg017/sqlite-dist' } 42 | ] 43 | } 44 | }) 45 | -------------------------------------------------------------------------------- /sample/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | platforms=( 4 | "macos-x86_64 x86_64-macos dylib" 5 | "macos-aarch64 aarch64-macos dylib" 6 | "linux-x86_64 x86_64-linux so" 7 | "linux-aarch64 aarch64-linux so" 8 | "windows-x86_64 x86_64-windows dll" 9 | ) 10 | 11 | for platform in "${platforms[@]}"; do 12 | read -r DIRECTORY_NAME ZIG_TARGET LOADABLE_SUFFIX <<< "$platform" 13 | 14 | mkdir -p dist/$DIRECTORY_NAME 15 | 16 | make \ 17 | TARGET=$ZIG_TARGET \ 18 | TARGET_LOADABLE=dist/$DIRECTORY_NAME/sample0.$LOADABLE_SUFFIX \ 19 | TARGET_STATIC=dist/$DIRECTORY_NAME/libsqlite_sample0.a \ 20 | TARGET_H=dist/$DIRECTORY_NAME/sqlite_sample.h \ 21 | loadable static h 22 | done 23 | 24 | exit 25 | 26 | 27 | mkdir -p dist/macos-x86_64 28 | mkdir -p dist/macos-aarch64 29 | mkdir -p dist/linux-x86_64 30 | mkdir -p dist/linux-aarch64 31 | mkdir -p dist/windows-x86_64 32 | mkdir -p dist/wasm32-emscripten 33 | 34 | make TARGET=x86_64-macos TARGET_LOADABLE=dist/macos-x86_64/sample0.dylib TARGET_STATIC=dist/macos-x86_64/libsqlite_sample0.a loadable static 35 | make TARGET=aarch64-macos TARGET_LOADABLE=dist/macos-aarch64/sample0.dylib TARGET_STATIC=dist/macos-aarch64/libsqlite_sample0.a loadable static 36 | make TARGET=x86_64-linux TARGET_LOADABLE=dist/linux-x86_64/sample0.so TARGET_STATIC=dist/linux-x86_64/libsqlite_sample0.a loadable static 37 | make TARGET=aarch64-linux TARGET_LOADABLE=dist/linux-aarch64/sample0.so TARGET_STATIC=dist/linux-aarch64/libsqlite_sample0.a loadable static 38 | make TARGET=x86_64-windows TARGET_LOADABLE=dist/windows-x86_64/sample0.dll TARGET_STATIC=dist/windows-x86_64/libsqlite_sample0.a loadable static 39 | touch dist/wasm32-emscripten/sqlite3.mjs dist/wasm32-emscripten/sqlite3.wasm 40 | -------------------------------------------------------------------------------- /src/spec.rs: -------------------------------------------------------------------------------- 1 | use semver::Version; 2 | use serde::Deserialize; 3 | 4 | #[derive(Deserialize)] 5 | pub struct SpecPackage { 6 | pub name: String, 7 | pub authors: Vec, 8 | pub license: String, 9 | pub description: String, 10 | pub homepage: String, 11 | pub repo: String, 12 | pub git_tag_format: Option, 13 | } 14 | 15 | impl SpecPackage { 16 | pub(crate) fn git_tag(&self, version: &Version) -> String { 17 | self.git_tag_format 18 | .as_ref() 19 | .map_or(version.to_string(), |f| { 20 | f.replace("$VERSION", &version.to_string()) 21 | }) 22 | } 23 | } 24 | 25 | #[derive(Deserialize)] 26 | pub struct TargetGithubRelease {} 27 | #[derive(Deserialize)] 28 | pub struct TargetSqlpkg {} 29 | #[derive(Deserialize)] 30 | pub struct TargetSpm {} 31 | 32 | #[derive(Deserialize)] 33 | pub struct TargetDatasette {} 34 | #[derive(Deserialize)] 35 | pub struct TargetPip { 36 | pub(crate) extra_init_py: Option, 37 | } 38 | 39 | #[derive(Deserialize)] 40 | pub struct TargetSqliteUtils {} 41 | 42 | #[derive(Deserialize)] 43 | pub struct TargetNpm {} 44 | 45 | #[derive(Deserialize)] 46 | pub struct TargetGem { 47 | pub module_name: String, 48 | } 49 | #[derive(Deserialize)] 50 | pub struct TargetAmalgamation { 51 | pub include: Vec, 52 | } 53 | 54 | #[derive(Deserialize)] 55 | pub struct Targets { 56 | pub github_releases: Option, 57 | pub sqlpkg: Option, 58 | pub spm: Option, 59 | pub pip: Option, 60 | pub datasette: Option, 61 | pub sqlite_utils: Option, 62 | pub npm: Option, 63 | pub gem: Option, 64 | pub amalgamation: Option, 65 | } 66 | #[derive(Deserialize)] 67 | pub struct Spec { 68 | pub package: SpecPackage, 69 | pub targets: Targets, 70 | } 71 | -------------------------------------------------------------------------------- /src/targets/sqlpkg.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::io::Result; 3 | use std::path::Path; 4 | 5 | use crate::build::{GeneratedAsset, GeneratedAssetKind, Project}; 6 | 7 | use serde::{Deserialize, Serialize}; 8 | 9 | #[derive(Debug, Deserialize, Serialize)] 10 | pub struct Asset { 11 | //path: 12 | pub pattern: Option, 13 | pub files: HashMap, 14 | pub checksums: HashMap, 15 | } 16 | 17 | #[derive(Debug, Deserialize, Serialize)] 18 | pub struct Sqlpkg { 19 | pub owner: String, 20 | pub name: String, 21 | pub version: String, 22 | pub homepage: String, 23 | pub repository: String, 24 | pub authors: Vec, 25 | pub license: String, 26 | pub description: String, 27 | pub keywords: Vec, 28 | pub symbols: Option>, 29 | pub assets: Asset, 30 | } 31 | 32 | pub(crate) fn write_sqlpkg(project: &Project, sqlpkg_dir: &Path) -> Result> { 33 | let sqlpkg = Sqlpkg { 34 | owner: project.spec.package.authors.join(", "), 35 | name: project.spec.package.name.clone(), 36 | version: project.version.to_string(), 37 | homepage: project.spec.package.homepage.clone(), 38 | repository: project.spec.package.repo.clone(), 39 | authors: project.spec.package.authors.clone(), 40 | license: project.spec.package.license.clone(), 41 | description: project.spec.package.description.clone(), 42 | keywords: vec![], // TODO keywords in spec? 43 | symbols: None, 44 | assets: Asset { 45 | pattern: None, 46 | files: HashMap::new(), 47 | checksums: HashMap::new(), 48 | }, 49 | }; 50 | let asset = GeneratedAsset::from( 51 | GeneratedAssetKind::Sqlpkg, 52 | &sqlpkg_dir.join("sqlpkg.json"), 53 | serde_json::to_string_pretty(&sqlpkg)?.as_bytes(), 54 | )?; 55 | Ok(vec![asset]) 56 | } 57 | -------------------------------------------------------------------------------- /src/targets/amalgamation.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Cursor, Result, Write}; 2 | use std::path::Path; 3 | 4 | use zip::write::FileOptions; 5 | 6 | use crate::spec::TargetAmalgamation; 7 | use crate::build::{create_targz, GeneratedAsset, GeneratedAssetKind, PlatformFile, Project}; 8 | 9 | pub(crate) fn write_amalgamation( 10 | project: &Project, 11 | amalgamation_dir: &Path, 12 | amalgamation_config: &TargetAmalgamation, 13 | ) -> Result> { 14 | let files: Result> = amalgamation_config 15 | .include 16 | .iter() 17 | .map(|relative_path| { 18 | let path = project.spec_directory.join(relative_path); 19 | let data = std::fs::read_to_string(&path)?; 20 | Ok(PlatformFile { 21 | name: relative_path.to_owned(), 22 | data: data.into(), 23 | metadata: Some(std::fs::metadata(&path)?), 24 | }) 25 | }) 26 | .collect(); 27 | let files = files?; 28 | let mut assets = vec![]; 29 | 30 | let targz = create_targz(files.iter().collect::>().as_ref())?; 31 | assets.push(GeneratedAsset::from( 32 | GeneratedAssetKind::Amalgamation, 33 | &amalgamation_dir.join(format!( 34 | "{}-{}-amalgamation.tar.gz", 35 | project.spec.package.name, project.version 36 | )), 37 | &targz, 38 | )?); 39 | 40 | let buffer = Cursor::new(Vec::new()); 41 | let mut zipfile = zip::ZipWriter::new(buffer); 42 | for file in files { 43 | let options = FileOptions::default().compression_method(zip::CompressionMethod::Deflated); 44 | zipfile.start_file(file.name, options)?; 45 | zipfile.write_all(&file.data)?; 46 | } 47 | assets.push(GeneratedAsset::from( 48 | GeneratedAssetKind::Amalgamation, 49 | &amalgamation_dir.join(format!( 50 | "{}-{}-amalgamation.zip", 51 | project.spec.package.name, project.version 52 | )), 53 | &zipfile.finish()?.into_inner(), 54 | )?); 55 | 56 | Ok(assets) 57 | } 58 | -------------------------------------------------------------------------------- /src/targets/spm.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | 3 | use crate::spec::Spec; 4 | use crate::build::{Cpu, GeneratedAsset, GeneratedAssetKind, Os}; 5 | 6 | #[derive(Debug, Serialize)] 7 | pub struct PlatformAsset { 8 | os: Os, 9 | cpu: Cpu, 10 | url: String, 11 | checksum_sha256: String, 12 | } 13 | 14 | #[derive(Debug, Serialize)] 15 | pub struct SpmJson { 16 | pub version: u32, 17 | pub description: String, 18 | pub loadable: Vec, 19 | #[serde(rename = "static")] 20 | pub static_: Option>, 21 | } 22 | 23 | use std::io::Result; 24 | use std::path::Path; 25 | 26 | pub(crate) fn write_spm( 27 | spec: &Spec, 28 | gh_release_assets: &[GeneratedAsset], 29 | spm_path: &Path, 30 | ) -> Result> { 31 | let loadable = gh_release_assets 32 | .iter() 33 | //.filter(|asset| matches!(asset.kind, GeneratedAssetKind::GithubReleaseLoadable(_))) 34 | .filter_map(|asset| match &asset.kind { 35 | GeneratedAssetKind::GithubReleaseLoadable(github_release) => Some(PlatformAsset { 36 | os: github_release.platform.0.clone(), 37 | cpu: github_release.platform.1.clone(), 38 | url: github_release.url.clone(), 39 | checksum_sha256: asset.checksum_sha256.clone(), 40 | }), 41 | _ => None, 42 | }) 43 | .collect(); 44 | let static_: Vec = gh_release_assets 45 | .iter() 46 | .filter_map(|asset| match &asset.kind { 47 | GeneratedAssetKind::GithubReleaseStatic(github_release) => Some(PlatformAsset { 48 | os: github_release.platform.0.clone(), 49 | cpu: github_release.platform.1.clone(), 50 | url: github_release.url.clone(), 51 | checksum_sha256: asset.checksum_sha256.clone(), 52 | }), 53 | _ => None, 54 | }) 55 | .collect(); 56 | let static_ = if static_.is_empty() { 57 | None 58 | } else { 59 | Some(static_) 60 | }; 61 | let spm_json = SpmJson { 62 | version: 0, 63 | description: spec.package.description.clone(), 64 | loadable, 65 | static_, 66 | }; 67 | let asset = GeneratedAsset::from( 68 | GeneratedAssetKind::Spm, 69 | &spm_path.join("spm.json"), 70 | serde_json::to_string_pretty(&spm_json)?.as_bytes(), 71 | )?; 72 | Ok(vec![asset]) 73 | } 74 | -------------------------------------------------------------------------------- /src/targets/gh_releases.rs: -------------------------------------------------------------------------------- 1 | use crate::build::PlatformDirectory; 2 | use crate::build::{ 3 | create_targz, GeneratedAsset, GeneratedAssetKind, GithubRelease, PlatformFile, Project, 4 | }; 5 | use std::io; 6 | use std::path::Path; 7 | 8 | fn create_loadable_github_release_asset( 9 | platform_directory: &PlatformDirectory, 10 | ) -> io::Result> { 11 | create_targz( 12 | &platform_directory 13 | .loadable_files 14 | .iter() 15 | .map(|l| &l.file) 16 | .collect::>(), 17 | ) 18 | } 19 | 20 | fn create_static_github_release_asset( 21 | platform_directory: &PlatformDirectory, 22 | ) -> Option>> { 23 | let mut targets = vec![]; 24 | targets.extend(&platform_directory.static_files); 25 | targets.extend(&platform_directory.header_files); 26 | if !targets.is_empty() { 27 | Some(create_targz(&targets)) 28 | } else { 29 | None 30 | } 31 | } 32 | 33 | fn github_release_artifact_name( 34 | name: &str, 35 | version: &str, 36 | os: &str, 37 | cpu: &str, 38 | artifact_type: &str, 39 | ) -> String { 40 | format!("{name}-{version}-{artifact_type}-{os}-{cpu}.tar.gz") 41 | } 42 | 43 | fn github_release_artifact_name_loadable( 44 | project: &Project, 45 | platform_dir: &PlatformDirectory, 46 | ) -> String { 47 | let name = project.spec.package.name.as_str(); 48 | let version = project.version.to_string(); 49 | let os = platform_dir.os.to_string(); 50 | let cpu = platform_dir.cpu.to_string(); 51 | github_release_artifact_name(name, &version, &os, &cpu, "loadable") 52 | } 53 | fn github_release_artifact_name_static( 54 | project: &Project, 55 | platform_dir: &PlatformDirectory, 56 | ) -> String { 57 | let name = project.spec.package.name.as_str(); 58 | let version = project.version.to_string(); 59 | let os = platform_dir.os.to_string(); 60 | let cpu = platform_dir.cpu.to_string(); 61 | github_release_artifact_name(name, &version, &os, &cpu, "static") 62 | } 63 | 64 | pub(crate) fn write_platform_files( 65 | project: &Project, 66 | ghreleases: &Path, 67 | ) -> Result, io::Error> { 68 | let mut loadable_assets = vec![]; 69 | let mut static_assets = vec![]; 70 | 71 | for platform_dir in &project.platform_directories { 72 | let ghl = create_loadable_github_release_asset(platform_dir)?; 73 | let lname = github_release_artifact_name_loadable(project, platform_dir); 74 | loadable_assets.push(GeneratedAsset::from( 75 | GeneratedAssetKind::GithubReleaseLoadable(GithubRelease { 76 | url: project.release_download_url(&lname), 77 | platform: (platform_dir.os.clone(), platform_dir.cpu.clone()), 78 | }), 79 | &ghreleases.join(lname), 80 | &ghl, 81 | )?); 82 | 83 | if let Some(ghs) = create_static_github_release_asset(platform_dir) { 84 | let sname = github_release_artifact_name_static(project, platform_dir); 85 | static_assets.push(GeneratedAsset::from( 86 | GeneratedAssetKind::GithubReleaseStatic(GithubRelease { 87 | url: project.release_download_url(&sname), 88 | platform: (platform_dir.os.clone(), platform_dir.cpu.clone()), 89 | }), 90 | &ghreleases.join(sname), 91 | &ghs?, 92 | )?); 93 | } 94 | } 95 | let mut generated_assets = vec![]; 96 | generated_assets.append(&mut loadable_assets); 97 | generated_assets.append(&mut static_assets); 98 | Ok(generated_assets) 99 | } 100 | -------------------------------------------------------------------------------- /src/targets/installer_sh.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod templates { 2 | use std::collections::HashSet; 3 | 4 | use crate::build::{Cpu, GeneratedAsset, GeneratedAssetKind, Os, Project}; 5 | 6 | struct Case { 7 | os: Os, 8 | cpu: Cpu, 9 | type_: String, 10 | url: String, 11 | checksum: String, 12 | } 13 | 14 | pub(crate) fn install_sh(project: &Project, assets: &[GeneratedAsset]) -> String { 15 | let mut targets = assets 16 | .iter() 17 | .filter_map(|asset| match &asset.kind { 18 | GeneratedAssetKind::GithubReleaseLoadable(gh_release) 19 | | GeneratedAssetKind::GithubReleaseStatic(gh_release) => Some(format!( 20 | "{}-{}", 21 | gh_release.platform.0.to_string(), 22 | gh_release.platform.1.to_string() 23 | )), 24 | _ => None, 25 | }) 26 | .collect::>() 27 | .into_iter() 28 | .collect::>(); 29 | targets.sort(); 30 | let targets = targets.join(", "); 31 | 32 | let cases: Vec<_> = assets 33 | .iter() 34 | .filter_map(|asset| match &asset.kind { 35 | GeneratedAssetKind::GithubReleaseLoadable(gh_release) => Some(Case { 36 | os: gh_release.platform.0.clone(), 37 | cpu: gh_release.platform.1.clone(), 38 | type_: "loadable".to_owned(), 39 | url: gh_release.url.to_string(), 40 | checksum: asset.checksum_sha256.clone(), 41 | }), 42 | GeneratedAssetKind::GithubReleaseStatic(gh_release) => Some(Case { 43 | os: gh_release.platform.0.clone(), 44 | cpu: gh_release.platform.1.clone(), 45 | type_: "static".to_owned(), 46 | url: gh_release.url.to_string(), 47 | checksum: asset.checksum_sha256.clone(), 48 | }), 49 | _ => None, 50 | }) 51 | .collect(); 52 | 53 | let usage = part_usage(&project.spec.package.name, project.version.to_string().as_str(), &targets); 54 | let current_target = part_current_target(); 55 | let process_arguments = part_process_arguments(); 56 | let main = part_main(cases); 57 | format!( 58 | r#"#!/bin/sh 59 | set -e 60 | 61 | if [ -n "$NO_COLOR" ]; then 62 | BOLD="" 63 | RESET="" 64 | else 65 | BOLD="\033[1m" 66 | RESET="\033[0m" 67 | fi 68 | 69 | {usage} 70 | 71 | {current_target} 72 | 73 | {process_arguments} 74 | 75 | {main} 76 | 77 | main "$@" 78 | "# 79 | ) 80 | } 81 | 82 | fn part_usage(project_name: &str, version: &str, targets: &str) -> String { 83 | // TODO: build commit, build date, project name NOT hello 84 | format!( 85 | r#" 86 | usage() {{ 87 | cat < String { 109 | r#" 110 | current_target() { 111 | if [ "$OS" = "Windows_NT" ]; then 112 | # TODO disambiguate between x86 and arm windows 113 | target="windows-x86_64" 114 | return 0 115 | fi 116 | case $(uname -sm) in 117 | "Darwin x86_64") target=macos-x86_64 ;; 118 | "Darwin arm64") target=macos-aarch64 ;; 119 | "Linux x86_64") target=linux-x86_64 ;; 120 | *) target=$(uname -sm);; 121 | esac 122 | } 123 | "# 124 | .to_owned() 125 | } 126 | fn part_process_arguments() -> String { 127 | (r#" 128 | process_arguments() { 129 | while [[ $# -gt 0 ]]; do 130 | case "$1" in 131 | --help) 132 | usage 133 | exit 0 134 | ;; 135 | --target=*) 136 | target="${1#*=}" 137 | ;; 138 | --prefix=*) 139 | prefix="${1#*=}" 140 | ;; 141 | --output=*) 142 | output="${1#*=}" 143 | ;; 144 | static|loadable) 145 | type="$1" 146 | ;; 147 | *) 148 | echo "Unrecognized option: $1" 149 | usage 150 | exit 1 151 | ;; 152 | esac 153 | shift 154 | done 155 | if [ -z "$type" ]; then 156 | type=loadable 157 | fi 158 | if [ "$type" != "static" ] && [ "$type" != "loadable" ]; then 159 | echo "Invalid type '$type'. It must be either 'static' or 'loadable'." 160 | usage 161 | exit 1 162 | fi 163 | if [ -z "$prefix" ]; then 164 | prefix="$PWD" 165 | fi 166 | if [ -z "$target" ]; then 167 | current_target 168 | fi 169 | } 170 | 171 | "#) 172 | .to_owned() 173 | } 174 | 175 | fn case(case: &Case) -> String { 176 | format!( 177 | r#" "{os}-{cpu}-{t}") 178 | url="{url}" 179 | checksum="{checksum}" 180 | ;;"#, 181 | os = case.os.to_string(), 182 | cpu = case.cpu.to_string(), 183 | t = case.type_, 184 | url = case.url, 185 | checksum = case.checksum 186 | ) 187 | } 188 | fn part_main(cases: Vec) -> String { 189 | let cases = cases.iter().map(case).collect::>().join("\n"); 190 | format!( 191 | r#" 192 | main() {{ 193 | local type="" 194 | local target="" 195 | local prefix="" 196 | local url="" 197 | local checksum="" 198 | local output="" 199 | 200 | process_arguments "$@" 201 | 202 | echo "${{BOLD}}Type${{RESET}}: $type" 203 | echo "${{BOLD}}Target${{RESET}}: $target" 204 | echo "${{BOLD}}Prefix${{RESET}}: $prefix" 205 | echo "${{BOLD}}Output${{RESET}}: $output" 206 | 207 | case "$target-$type" in 208 | {cases} 209 | *) 210 | echo "Unsupported platform $target" 1>&2 211 | exit 1 212 | ;; 213 | esac 214 | 215 | extension="\${{url##*.}}" 216 | 217 | if [ "$extension" = "zip" ]; then 218 | tmpfile="$prefix/tmp.zip" 219 | else 220 | tmpfile="$prefix/tmp.tar.gz" 221 | fi 222 | 223 | curl --fail --location --progress-bar --output "$tmpfile" "$url" 224 | 225 | if ! echo "$checksum $tmpfile" | shasum -a 256 --check --status; then 226 | echo "Checksum fail!" 1>&2 227 | rm $tmpfile 228 | exit 1 229 | fi 230 | 231 | if [ "$extension" = "zip" ]; then 232 | unzip "$tmpfile" -d $prefix 233 | rm $tmpfile 234 | else 235 | if [ -z "$output" ]; then 236 | tar -xzf "$tmpfile" -C $prefix 237 | else 238 | tar -xOzf "$tmpfile" > $output 239 | fi 240 | rm $tmpfile 241 | fi 242 | 243 | echo "✅ $target $type binaries installed at $prefix." 244 | }} 245 | 246 | "# 247 | ) 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/targets/gem.rs: -------------------------------------------------------------------------------- 1 | use crate::spec::TargetGem; 2 | use crate::build::{Cpu, Os}; 3 | use crate::build::{GeneratedAsset, GeneratedAssetKind, Project}; 4 | use flate2::write::GzEncoder; 5 | use flate2::Compression; 6 | use serde::{Deserialize, Serialize}; 7 | use sha2::{Digest, Sha256, Sha512}; 8 | use std::io::{self, Cursor}; 9 | use std::path::Path; 10 | use std::{collections::HashMap, io::Write}; 11 | use tar::{Builder, Header}; 12 | 13 | #[derive(Debug, Deserialize, Serialize)] 14 | pub struct Gemspec { 15 | name: String, 16 | version: String, 17 | // https://guides.rubygems.org/specification-reference/#authors= 18 | authors: Vec, 19 | // https://guides.rubygems.org/specification-reference/#email 20 | email: Vec, 21 | homepage: String, 22 | summary: String, 23 | description: String, 24 | licenses: Vec, 25 | // https://guides.rubygems.org/specification-reference/#metadata 26 | metadata: HashMap, 27 | platform: String, 28 | module_name: String, 29 | } 30 | 31 | fn gem_checksum_sha256(data: &[u8]) -> String { 32 | base16ct::lower::encode_string(&Sha256::digest(data)) 33 | } 34 | fn gem_checksum_sha512(data: &[u8]) -> String { 35 | base16ct::lower::encode_string(&Sha512::digest(data)) 36 | } 37 | 38 | fn gem_metadata_list_helper(items: Vec) -> String { 39 | items 40 | .iter() 41 | .map(|item| { 42 | format!("- {}", { 43 | serde_json::to_string(&serde_json::Value::String(item.to_owned())) 44 | .expect("String JSON to serialize") 45 | }) 46 | }) 47 | .collect::>() 48 | .join("\n") 49 | } 50 | 51 | fn ruby_platform(os: &Os, cpu: &Cpu) -> String { 52 | let os = match os { 53 | Os::Macos => "darwin", 54 | Os::Linux => "linux", 55 | Os::Windows => "mingw32", 56 | _ => unreachable!( 57 | "Invalid gem OS {:?} provided, should have been filtered out", 58 | os, 59 | ), 60 | }; 61 | let cpu = match cpu { 62 | Cpu::X86_64 => "x86_64", 63 | Cpu::Aarch64 => "arm64", 64 | _ => unreachable!( 65 | "Invalid gem CPU {:?} provided, should have been filtered out", 66 | cpu 67 | ), 68 | }; 69 | format!("{cpu}-{os}") 70 | } 71 | #[allow(clippy::too_many_arguments)] 72 | fn gem_metadata_template( 73 | os: &Os, 74 | cpu: &Cpu, 75 | name: &str, 76 | version: &str, 77 | files: Vec, 78 | email: &str, 79 | authors: Vec, 80 | licenses: Vec, 81 | description: &str, 82 | summary: &str, 83 | homepage: &str, 84 | ) -> String { 85 | let ruby_platform = ruby_platform(os, cpu); 86 | let date = chrono::offset::Local::now().format("%Y-%m-%d").to_string(); 87 | let authors = gem_metadata_list_helper(authors); 88 | let files = gem_metadata_list_helper(files); 89 | let licenses = gem_metadata_list_helper(licenses); 90 | 91 | // ? 92 | let version = version.replace('-', "."); 93 | format!( 94 | r#"--- !ruby/object:Gem::Specification 95 | name: {name} 96 | version: !ruby/object:Gem::Version 97 | version: {version} 98 | platform: {ruby_platform} 99 | authors: 100 | {authors} 101 | autorequire: 102 | bindir: bin 103 | cert_chain: [] 104 | date: {date} 00:00:00.000000000 Z 105 | dependencies: [] 106 | description: '{description}' 107 | summary: '{summary}' 108 | email: 109 | - {email} 110 | executables: [] 111 | extensions: [] 112 | extra_rdoc_files: [] 113 | files: 114 | {files} 115 | homepage: '{homepage}' 116 | licenses: 117 | {licenses} 118 | post_install_message: 119 | rdoc_options: [] 120 | require_paths: 121 | - lib 122 | required_ruby_version: !ruby/object:Gem::Requirement 123 | requirements: 124 | - - ">=" 125 | - !ruby/object:Gem::Version 126 | version: '0' 127 | required_rubygems_version: !ruby/object:Gem::Requirement 128 | requirements: 129 | - - ">=" 130 | - !ruby/object:Gem::Version 131 | version: '0' 132 | requirements: [] 133 | rubygems_version: 3.4.10 134 | signing_key: 135 | specification_version: 4 136 | test_files: [] 137 | "# 138 | ) 139 | } 140 | 141 | fn checksums_yaml_gz(metadata_gz: &[u8], data_targz: &[u8]) -> io::Result> { 142 | let checksums_yaml = format!( 143 | r#"--- 144 | SHA256: 145 | metadata.gz: '{}' 146 | data.tar.gz: '{}' 147 | SHA512: 148 | metadata.gz: '{}' 149 | data.tar.gz: '{}' 150 | "#, 151 | gem_checksum_sha256(metadata_gz), 152 | gem_checksum_sha256(data_targz), 153 | gem_checksum_sha512(metadata_gz), 154 | gem_checksum_sha512(data_targz), 155 | ); 156 | let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); 157 | encoder.write_all(checksums_yaml.as_bytes())?; 158 | encoder.finish() 159 | } 160 | 161 | pub struct Gem { 162 | library_tarball: Builder>>, 163 | library_filenames: Vec, 164 | } 165 | 166 | impl Gem { 167 | pub fn new() -> Self { 168 | let tar_gz: Vec = Vec::new(); 169 | let enc = GzEncoder::new(tar_gz, Compression::default()); 170 | let tar = tar::Builder::new(enc); 171 | Self { 172 | library_tarball: tar, 173 | library_filenames: vec![], 174 | } 175 | } 176 | pub fn write_library_file(&mut self, path: &str, data: &[u8]) -> io::Result<()> { 177 | let mut header = Header::new_gnu(); 178 | 179 | header.set_path(path)?; 180 | header.set_size(data.len() as u64); 181 | header.set_mode(0o777); 182 | header.set_cksum(); 183 | self.library_tarball.append::<&[u8]>(&header, data)?; 184 | self.library_filenames.push(path.to_string()); 185 | Ok(()) 186 | } 187 | 188 | fn metadata_gz(&self, os: &Os, cpu: &Cpu, project: &Project) -> io::Result> { 189 | let metadata = gem_metadata_template( 190 | os, 191 | cpu, 192 | &project.spec.package.name, 193 | project.version.to_string().as_str(), 194 | self.library_filenames.clone(), 195 | "TODO", 196 | project.spec.package.authors.clone(), 197 | vec![project.spec.package.license.clone()], 198 | &project.spec.package.description, 199 | &project.spec.package.description, 200 | "https://github.com/TODO", 201 | ); 202 | let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); 203 | encoder.write_all(metadata.as_bytes())?; 204 | encoder.finish() 205 | } 206 | 207 | pub fn complete( 208 | mut self, 209 | os: &Os, 210 | cpu: &Cpu, 211 | project: &Project, 212 | ) -> io::Result<(String, Vec)> { 213 | let mut gem_tar: Vec = Vec::new(); 214 | { 215 | let mut tar = tar::Builder::new(Cursor::new(&mut gem_tar)); 216 | let mut header = Header::new_gnu(); 217 | 218 | let metadata_gz = self.metadata_gz(os, cpu, project)?; 219 | header.set_path("metadata.gz")?; 220 | header.set_size(metadata_gz.len() as u64); 221 | header.set_cksum(); 222 | tar.append::<&[u8]>(&header, metadata_gz.as_ref())?; 223 | 224 | self.library_tarball.finish()?; 225 | let data_tar_gz = self.library_tarball.into_inner()?.finish()?; 226 | header.set_path("data.tar.gz")?; 227 | header.set_size(data_tar_gz.len() as u64); 228 | header.set_cksum(); 229 | tar.append::<&[u8]>(&header, data_tar_gz.as_ref())?; 230 | 231 | let checksums_yaml_gz = checksums_yaml_gz(&metadata_gz, &data_tar_gz)?; 232 | header.set_path("checksums.yaml.gz")?; 233 | header.set_size(checksums_yaml_gz.len() as u64); 234 | header.set_cksum(); 235 | tar.append::<&[u8]>(&header, checksums_yaml_gz.as_ref())?; 236 | 237 | tar.finish()?; 238 | } 239 | Ok(( 240 | format!( 241 | "{}-{}-{}.gem", 242 | project.spec.package.name, 243 | // ? 244 | project.version.to_string().replace('-', "."), 245 | ruby_platform(os, cpu) 246 | ), 247 | gem_tar, 248 | )) 249 | } 250 | } 251 | 252 | pub(crate) fn write_gems( 253 | project: &Project, 254 | gem_path: &Path, 255 | gem_config: &TargetGem, 256 | ) -> io::Result> { 257 | let mut assets = vec![]; 258 | for platform_dir in &project.platform_directories { 259 | if !(matches!(platform_dir.os, Os::Linux | Os::Macos | Os::Windows) 260 | && matches!(platform_dir.cpu, Cpu::X86_64 | Cpu::Aarch64)) 261 | { 262 | continue; 263 | } 264 | let mut gem = Gem::new(); 265 | assert!(!platform_dir.loadable_files.is_empty()); 266 | let loadable_name = platform_dir.loadable_files[0].file.name.clone(); 267 | let entrypoint = &platform_dir.loadable_files[0].file_stem; 268 | 269 | gem.write_library_file( 270 | format!("lib/{}", loadable_name).as_str(), 271 | platform_dir.loadable_files[0].file.data.as_ref(), 272 | )?; 273 | 274 | gem.write_library_file( 275 | format!("lib/{}.rb", project.spec.package.name.replace('-', "_")).as_str(), 276 | templates::lib_rb(&project.version, entrypoint, &gem_config.module_name).as_bytes(), 277 | )?; 278 | let (gem_name, data) = gem.complete(&platform_dir.os, &platform_dir.cpu, project)?; 279 | assets.push(GeneratedAsset::from( 280 | GeneratedAssetKind::Gem((platform_dir.os.clone(), platform_dir.cpu.clone())), 281 | &gem_path.join(gem_name), 282 | &data, 283 | )?); 284 | } 285 | Ok(assets) 286 | } 287 | 288 | mod templates { 289 | use semver::Version; 290 | 291 | pub(crate) fn lib_rb(version: &Version, entrypoint: &str, module_name: &str) -> String { 292 | format!( 293 | r#" 294 | module {module_name} 295 | class Error < StandardError; end 296 | VERSION = "{version}" 297 | def self.loadable_path 298 | File.expand_path('{entrypoint}', File.dirname(__FILE__)) 299 | end 300 | def self.load(db) 301 | db.load_extension(self.loadable_path) 302 | end 303 | end 304 | 305 | "# 306 | ) 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by dist: https://opensource.axo.dev/cargo-dist/ 2 | # 3 | # Copyright 2022-2024, axodotdev 4 | # SPDX-License-Identifier: MIT or Apache-2.0 5 | # 6 | # CI that: 7 | # 8 | # * checks for a Git Tag that looks like a release 9 | # * builds artifacts with dist (archives, installers, hashes) 10 | # * uploads those artifacts to temporary workflow zip 11 | # * on success, uploads the artifacts to a GitHub Release 12 | # 13 | # Note that the GitHub Release will be created with a generated 14 | # title/body based on your changelogs. 15 | 16 | name: Release 17 | permissions: 18 | "contents": "write" 19 | 20 | # This task will run whenever you push a git tag that looks like a version 21 | # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. 22 | # Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where 23 | # PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION 24 | # must be a Cargo-style SemVer Version (must have at least major.minor.patch). 25 | # 26 | # If PACKAGE_NAME is specified, then the announcement will be for that 27 | # package (erroring out if it doesn't have the given version or isn't dist-able). 28 | # 29 | # If PACKAGE_NAME isn't specified, then the announcement will be for all 30 | # (dist-able) packages in the workspace with that version (this mode is 31 | # intended for workspaces with only one dist-able package, or with all dist-able 32 | # packages versioned/released in lockstep). 33 | # 34 | # If you push multiple tags at once, separate instances of this workflow will 35 | # spin up, creating an independent announcement for each one. However, GitHub 36 | # will hard limit this to 3 tags per commit, as it will assume more tags is a 37 | # mistake. 38 | # 39 | # If there's a prerelease-style suffix to the version, then the release(s) 40 | # will be marked as a prerelease. 41 | on: 42 | pull_request: 43 | push: 44 | tags: 45 | - '**[0-9]+.[0-9]+.[0-9]+*' 46 | 47 | jobs: 48 | # Run 'dist plan' (or host) to determine what tasks we need to do 49 | plan: 50 | runs-on: "ubuntu-20.04" 51 | outputs: 52 | val: ${{ steps.plan.outputs.manifest }} 53 | tag: ${{ !github.event.pull_request && github.ref_name || '' }} 54 | tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} 55 | publishing: ${{ !github.event.pull_request }} 56 | env: 57 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | steps: 59 | - uses: actions/checkout@v4 60 | with: 61 | submodules: recursive 62 | - name: Install dist 63 | # we specify bash to get pipefail; it guards against the `curl` command 64 | # failing. otherwise `sh` won't catch that `curl` returned non-0 65 | shell: bash 66 | run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.28.0/cargo-dist-installer.sh | sh" 67 | - name: Cache dist 68 | uses: actions/upload-artifact@v4 69 | with: 70 | name: cargo-dist-cache 71 | path: ~/.cargo/bin/dist 72 | # sure would be cool if github gave us proper conditionals... 73 | # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible 74 | # functionality based on whether this is a pull_request, and whether it's from a fork. 75 | # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* 76 | # but also really annoying to build CI around when it needs secrets to work right.) 77 | - id: plan 78 | run: | 79 | dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json 80 | echo "dist ran successfully" 81 | cat plan-dist-manifest.json 82 | echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" 83 | - name: "Upload dist-manifest.json" 84 | uses: actions/upload-artifact@v4 85 | with: 86 | name: artifacts-plan-dist-manifest 87 | path: plan-dist-manifest.json 88 | 89 | # Build and packages all the platform-specific things 90 | build-local-artifacts: 91 | name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) 92 | # Let the initial task tell us to not run (currently very blunt) 93 | needs: 94 | - plan 95 | if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} 96 | strategy: 97 | fail-fast: false 98 | # Target platforms/runners are computed by dist in create-release. 99 | # Each member of the matrix has the following arguments: 100 | # 101 | # - runner: the github runner 102 | # - dist-args: cli flags to pass to dist 103 | # - install-dist: expression to run to install dist on the runner 104 | # 105 | # Typically there will be: 106 | # - 1 "global" task that builds universal installers 107 | # - N "local" tasks that build each platform's binaries and platform-specific installers 108 | matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} 109 | runs-on: ${{ matrix.runner }} 110 | container: ${{ matrix.container && matrix.container.image || null }} 111 | env: 112 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 113 | BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json 114 | steps: 115 | - name: enable windows longpaths 116 | run: | 117 | git config --global core.longpaths true 118 | - uses: actions/checkout@v4 119 | with: 120 | submodules: recursive 121 | - name: Install Rust non-interactively if not already installed 122 | if: ${{ matrix.container }} 123 | run: | 124 | if ! command -v cargo > /dev/null 2>&1; then 125 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 126 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH 127 | fi 128 | - name: Install dist 129 | run: ${{ matrix.install_dist.run }} 130 | # Get the dist-manifest 131 | - name: Fetch local artifacts 132 | uses: actions/download-artifact@v4 133 | with: 134 | pattern: artifacts-* 135 | path: target/distrib/ 136 | merge-multiple: true 137 | - name: Install dependencies 138 | run: | 139 | ${{ matrix.packages_install }} 140 | - name: Build artifacts 141 | run: | 142 | # Actually do builds and make zips and whatnot 143 | dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json 144 | echo "dist ran successfully" 145 | - id: cargo-dist 146 | name: Post-build 147 | # We force bash here just because github makes it really hard to get values up 148 | # to "real" actions without writing to env-vars, and writing to env-vars has 149 | # inconsistent syntax between shell and powershell. 150 | shell: bash 151 | run: | 152 | # Parse out what we just built and upload it to scratch storage 153 | echo "paths<> "$GITHUB_OUTPUT" 154 | dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT" 155 | echo "EOF" >> "$GITHUB_OUTPUT" 156 | 157 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 158 | - name: "Upload artifacts" 159 | uses: actions/upload-artifact@v4 160 | with: 161 | name: artifacts-build-local-${{ join(matrix.targets, '_') }} 162 | path: | 163 | ${{ steps.cargo-dist.outputs.paths }} 164 | ${{ env.BUILD_MANIFEST_NAME }} 165 | 166 | # Build and package all the platform-agnostic(ish) things 167 | build-global-artifacts: 168 | needs: 169 | - plan 170 | - build-local-artifacts 171 | runs-on: "ubuntu-20.04" 172 | env: 173 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 174 | BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json 175 | steps: 176 | - uses: actions/checkout@v4 177 | with: 178 | submodules: recursive 179 | - name: Install cached dist 180 | uses: actions/download-artifact@v4 181 | with: 182 | name: cargo-dist-cache 183 | path: ~/.cargo/bin/ 184 | - run: chmod +x ~/.cargo/bin/dist 185 | # Get all the local artifacts for the global tasks to use (for e.g. checksums) 186 | - name: Fetch local artifacts 187 | uses: actions/download-artifact@v4 188 | with: 189 | pattern: artifacts-* 190 | path: target/distrib/ 191 | merge-multiple: true 192 | - id: cargo-dist 193 | shell: bash 194 | run: | 195 | dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json 196 | echo "dist ran successfully" 197 | 198 | # Parse out what we just built and upload it to scratch storage 199 | echo "paths<> "$GITHUB_OUTPUT" 200 | jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" 201 | echo "EOF" >> "$GITHUB_OUTPUT" 202 | 203 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 204 | - name: "Upload artifacts" 205 | uses: actions/upload-artifact@v4 206 | with: 207 | name: artifacts-build-global 208 | path: | 209 | ${{ steps.cargo-dist.outputs.paths }} 210 | ${{ env.BUILD_MANIFEST_NAME }} 211 | # Determines if we should publish/announce 212 | host: 213 | needs: 214 | - plan 215 | - build-local-artifacts 216 | - build-global-artifacts 217 | # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) 218 | if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} 219 | env: 220 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 221 | runs-on: "ubuntu-20.04" 222 | outputs: 223 | val: ${{ steps.host.outputs.manifest }} 224 | steps: 225 | - uses: actions/checkout@v4 226 | with: 227 | submodules: recursive 228 | - name: Install cached dist 229 | uses: actions/download-artifact@v4 230 | with: 231 | name: cargo-dist-cache 232 | path: ~/.cargo/bin/ 233 | - run: chmod +x ~/.cargo/bin/dist 234 | # Fetch artifacts from scratch-storage 235 | - name: Fetch artifacts 236 | uses: actions/download-artifact@v4 237 | with: 238 | pattern: artifacts-* 239 | path: target/distrib/ 240 | merge-multiple: true 241 | - id: host 242 | shell: bash 243 | run: | 244 | dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json 245 | echo "artifacts uploaded and released successfully" 246 | cat dist-manifest.json 247 | echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" 248 | - name: "Upload dist-manifest.json" 249 | uses: actions/upload-artifact@v4 250 | with: 251 | # Overwrite the previous copy 252 | name: artifacts-dist-manifest 253 | path: dist-manifest.json 254 | # Create a GitHub Release while uploading all files to it 255 | - name: "Download GitHub Artifacts" 256 | uses: actions/download-artifact@v4 257 | with: 258 | pattern: artifacts-* 259 | path: artifacts 260 | merge-multiple: true 261 | - name: Cleanup 262 | run: | 263 | # Remove the granular manifests 264 | rm -f artifacts/*-dist-manifest.json 265 | - name: Create GitHub Release 266 | env: 267 | PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" 268 | ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" 269 | ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" 270 | RELEASE_COMMIT: "${{ github.sha }}" 271 | run: | 272 | # Write and read notes from a file to avoid quoting breaking things 273 | echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt 274 | 275 | gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* 276 | 277 | announce: 278 | needs: 279 | - plan 280 | - host 281 | # use "always() && ..." to allow us to wait for all publish jobs while 282 | # still allowing individual publish jobs to skip themselves (for prereleases). 283 | # "host" however must run to completion, no skipping allowed! 284 | if: ${{ always() && needs.host.result == 'success' }} 285 | runs-on: "ubuntu-20.04" 286 | env: 287 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 288 | steps: 289 | - uses: actions/checkout@v4 290 | with: 291 | submodules: recursive 292 | -------------------------------------------------------------------------------- /src/targets/pip.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{self, Cursor, Write}, 3 | path::Path, 4 | }; 5 | 6 | use crate::build::{AssetPipWheel, Cpu, GeneratedAsset, GeneratedAssetKind, Os, Project}; 7 | use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; 8 | use semver::Version; 9 | use sha2::{Digest, Sha256}; 10 | use zip::{result::ZipError, write::FileOptions, ZipWriter}; 11 | 12 | mod templates { 13 | use crate::{targets::pip::platform_target_tag, build::Cpu, build::Os}; 14 | 15 | use super::PipPackage; 16 | 17 | pub(crate) fn dist_info_metadata(pkg: &PipPackage) -> String { 18 | let name = &pkg.package_name; 19 | let version = &pkg.package_version; 20 | let extra_metadata: String = if pkg.extra_metadata.len() > 1 { 21 | let mut s = String::new(); 22 | for (key, value) in &pkg.extra_metadata { 23 | s += format!("{key}: {value}\n").as_str(); 24 | } 25 | s 26 | } else { 27 | "".to_owned() 28 | }; 29 | format!( 30 | "Metadata-Version: 2.1 31 | Name: {name} 32 | Version: {version} 33 | Home-page: https://TODO.com 34 | Author: TODO 35 | License: MIT License, Apache License, Version 2.0 36 | Description-Content-Type: text/markdown 37 | {extra_metadata} 38 | 39 | TODO readme" 40 | ) 41 | } 42 | 43 | pub(crate) fn dist_info_entrypoints(entrypoints: &Vec<(String, String)>) -> String { 44 | let mut txt = String::new(); 45 | for (key, value) in entrypoints { 46 | txt += format!("[{key}]\n").as_str(); 47 | txt += value; 48 | txt += "\n\n"; 49 | } 50 | 51 | txt 52 | } 53 | pub(crate) fn dist_info_wheel(platform: Option<(&Os, &Cpu)>) -> String { 54 | let name = env!("CARGO_PKG_NAME"); 55 | let version = env!("CARGO_PKG_VERSION"); 56 | let platform_tag = match platform { 57 | Some((os, cpu)) => platform_target_tag(os, cpu), 58 | None => "any".to_owned(), 59 | }; 60 | let tag = format!("py3-none-{platform_tag}"); 61 | format!( 62 | "Wheel-Version: 1.0 63 | Generator: {name} {version} 64 | Root-Is-Purelib: false 65 | Tag: {tag}", 66 | ) 67 | } 68 | pub(crate) fn dist_info_top_level_txt(pkg: &PipPackage) -> String { 69 | format!("{}\n", pkg.python_package_name) 70 | } 71 | 72 | pub(crate) fn dist_info_record(pkg: &PipPackage, record_path: &str) -> String { 73 | let mut record = String::new(); 74 | for file in &pkg.written_files { 75 | record.push_str(format!("{},sha256={},{}\n", file.path, file.hash, file.size).as_str()); 76 | } 77 | 78 | // RECORD one can be empty 79 | record.push_str(format!("{},,\n", record_path).as_str()); 80 | 81 | record 82 | } 83 | pub(crate) fn base_init_py(pkg: &PipPackage, entrypoint: &str) -> String { 84 | let version = &pkg.package_version; 85 | let package_name = &pkg.package_name; 86 | format!( 87 | r#" 88 | from os import path 89 | import sqlite3 90 | 91 | __version__ = "{version}" 92 | __version_info__ = tuple(__version__.split(".")) 93 | 94 | def loadable_path(): 95 | """ Returns the full path to the {package_name} loadable SQLite extension bundled with this package """ 96 | 97 | loadable_path = path.join(path.dirname(__file__), "{entrypoint}") 98 | return path.normpath(loadable_path) 99 | 100 | def load(conn: sqlite3.Connection) -> None: 101 | """ Load the {package_name} SQLite extension into the given database connection. """ 102 | 103 | conn.load_extension(loadable_path()) 104 | 105 | "#, 106 | ) 107 | } 108 | 109 | pub(crate) fn sqlite_utils_init_py(dep_pkg: &PipPackage) -> String { 110 | let dep_library = dep_pkg.python_package_name.clone(); 111 | let version = dep_pkg.package_version.clone(); 112 | format!( 113 | r#" 114 | from sqlite_utils import hookimpl 115 | import {dep_library} 116 | 117 | __version__ = "{version}" 118 | __version_info__ = tuple(__version__.split(".")) 119 | 120 | @hookimpl 121 | def prepare_connection(conn): 122 | conn.enable_load_extension(True) 123 | {dep_library}.load(conn) 124 | conn.enable_load_extension(False) 125 | "# 126 | ) 127 | } 128 | 129 | pub(crate) fn datasette_init_py(dep_pkg: &PipPackage) -> String { 130 | let dep_library = dep_pkg.python_package_name.clone(); 131 | let version = dep_pkg.package_version.clone(); 132 | format!( 133 | r#" 134 | from datasette import hookimpl 135 | import {dep_library} 136 | 137 | __version__ = "{version}" 138 | __version_info__ = tuple(__version__.split(".")) 139 | 140 | @hookimpl 141 | def prepare_connection(conn): 142 | conn.enable_load_extension(True) 143 | {dep_library}.load(conn) 144 | conn.enable_load_extension(False) 145 | "#, 146 | ) 147 | } 148 | } 149 | 150 | pub struct PipPackageFile { 151 | path: String, 152 | hash: String, 153 | size: usize, 154 | } 155 | 156 | impl PipPackageFile { 157 | fn new(path: &str, data: &[u8]) -> Self { 158 | let hash = URL_SAFE_NO_PAD.encode(Sha256::digest(data)); 159 | Self { 160 | path: path.to_owned(), 161 | hash, 162 | size: data.len(), 163 | } 164 | } 165 | } 166 | 167 | fn semver_to_pip_version(v: &Version) -> String { 168 | match ( 169 | (!v.pre.is_empty()).then(|| v.pre.clone()), 170 | (!v.build.is_empty()).then(|| v.build.clone()), 171 | ) { 172 | (None, None) => v.to_string(), 173 | // ??? 174 | (None, Some(_build)) => v.to_string(), 175 | (Some(pre), None) => { 176 | let base = Version::new(v.major, v.minor, v.patch).to_string(); 177 | let (a, b) = pre.split_once('.').unwrap(); 178 | match a { 179 | "alpha" => format!("{base}a{b}"), 180 | "beta" => format!("{base}b{b}"), 181 | "rc" => format!("{base}rc{b}"), 182 | _ => todo!(), 183 | } 184 | } 185 | (Some(_pre), Some(_build)) => todo!(), 186 | } 187 | /*if v.pre.is_empty() && v.build.is_empty() { 188 | v.to_string() 189 | } else if v.build.is_empty() { 190 | }*/ 191 | } 192 | 193 | pub fn platform_target_tag(os: &Os, cpu: &Cpu) -> String { 194 | match (os, cpu) { 195 | (Os::Macos, Cpu::X86_64) => "macosx_10_6_x86_64".to_owned(), 196 | (Os::Macos, Cpu::Aarch64) => "macosx_11_0_arm64".to_owned(), 197 | (Os::Linux, Cpu::X86_64) => { 198 | "manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux1_x86_64".to_owned() 199 | } 200 | (Os::Linux, Cpu::Aarch64) => "manylinux_2_17_aarch64.manylinux2014_aarch64".to_owned(), 201 | (Os::Windows, Cpu::X86_64) => "win_amd64".to_owned(), 202 | _ => { 203 | unreachable!( 204 | "Invalid pip platform {:?}-{:?} provided, should have been filtered out", 205 | os, cpu 206 | ) 207 | } 208 | } 209 | } 210 | 211 | pub struct PipPackage { 212 | pub zipfile: ZipWriter>>, 213 | // as-is, with dashes, not python code safe 214 | pub package_name: String, 215 | // dashes replaced with underscores 216 | pub python_package_name: String, 217 | 218 | // not semver, but the special pip version string (ex 1.2a3) 219 | pub package_version: String, 220 | pub written_files: Vec, 221 | 222 | pub entrypoints: Vec<(String, String)>, 223 | pub extra_metadata: Vec<(String, String)>, 224 | } 225 | 226 | impl PipPackage { 227 | pub fn new>(package_name: S, package_version: &Version) -> Self { 228 | let buffer = Cursor::new(Vec::new()); 229 | let zipfile = zip::ZipWriter::new(buffer); 230 | let package_name = package_name.into(); 231 | Self { 232 | zipfile, 233 | package_name: package_name.clone(), 234 | python_package_name: package_name.replace('-', "_"), 235 | package_version: semver_to_pip_version(package_version), 236 | written_files: vec![], 237 | entrypoints: vec![], 238 | extra_metadata: vec![], 239 | } 240 | } 241 | 242 | pub fn add_entrypoint(&mut self, key: &str, value: &str) { 243 | self.entrypoints.push((key.to_owned(), value.to_owned())); 244 | } 245 | 246 | pub fn wheel_name(&self, platform: Option<(&Os, &Cpu)>) -> String { 247 | let name = &self.python_package_name; 248 | let version = &self.package_version; 249 | let python_tag = "py3"; 250 | let abi_tag = "none"; 251 | let platform_tag = match platform { 252 | Some((os, cpu)) => platform_target_tag(os, cpu), 253 | None => "any".to_owned(), 254 | }; 255 | format!("{name}-{version}-{python_tag}-{abi_tag}-{platform_tag}.whl") 256 | } 257 | 258 | fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), ZipError> { 259 | let options = FileOptions::default().compression_method(zip::CompressionMethod::Stored); 260 | self.zipfile.start_file(path, options)?; 261 | self.zipfile.write_all(data)?; 262 | self.written_files.push(PipPackageFile::new(path, data)); 263 | Ok(()) 264 | } 265 | 266 | pub fn write_library_file(&mut self, path: &str, data: &[u8]) -> Result<(), ZipError> { 267 | self.write_file( 268 | format!("{}/{}", self.python_package_name, path).as_str(), 269 | data, 270 | ) 271 | } 272 | 273 | fn dist_info_file(&self, file: &str) -> String { 274 | format!( 275 | "{}-{}.dist-info/{}", 276 | self.python_package_name, self.package_version, file 277 | ) 278 | } 279 | 280 | fn write_dist_info_metadata(&mut self) -> Result<(), ZipError> { 281 | self.write_file( 282 | self.dist_info_file("METADATA").as_str(), 283 | templates::dist_info_metadata(self).as_bytes(), 284 | ) 285 | } 286 | 287 | fn write_dist_info_record(&mut self) -> Result<(), ZipError> { 288 | let record_path = self.dist_info_file("RECORD"); 289 | self.write_file( 290 | &record_path, 291 | templates::dist_info_record(self, &record_path).as_bytes(), 292 | ) 293 | } 294 | fn write_dist_info_top_level_txt(&mut self) -> Result<(), ZipError> { 295 | self.write_file( 296 | self.dist_info_file("top_level.txt").as_str(), 297 | templates::dist_info_top_level_txt(self).as_bytes(), 298 | ) 299 | } 300 | fn write_dist_info_wheel(&mut self, platform: Option<(&Os, &Cpu)>) -> Result<(), ZipError> { 301 | self.write_file( 302 | self.dist_info_file("WHEEL").as_str(), 303 | templates::dist_info_wheel(platform).as_bytes(), 304 | ) 305 | } 306 | fn write_dist_info_entrypoints(&mut self) -> Result<(), ZipError> { 307 | self.write_file( 308 | self.dist_info_file("entry_points.txt").as_str(), 309 | templates::dist_info_entrypoints(&self.entrypoints).as_bytes(), 310 | ) 311 | } 312 | 313 | pub fn end(mut self, platform: Option<(&Os, &Cpu)>) -> Result>, ZipError> { 314 | self.write_dist_info_metadata()?; 315 | self.write_dist_info_wheel(platform)?; 316 | if !self.entrypoints.is_empty() { 317 | self.write_dist_info_entrypoints()?; 318 | } 319 | self.write_dist_info_top_level_txt()?; 320 | self.write_dist_info_record()?; 321 | self.zipfile.finish() 322 | } 323 | } 324 | 325 | use thiserror::Error; 326 | 327 | #[derive(Error, Debug)] 328 | pub enum PipBuildError { 329 | #[error("Zipfile error: {0}")] 330 | ZipError(#[from] ZipError), 331 | #[error("I/O error: {0}")] 332 | IOError(#[from] io::Error), 333 | } 334 | 335 | pub(crate) fn write_base_packages( 336 | project: &Project, 337 | pip_path: &Path, 338 | ) -> Result, PipBuildError> { 339 | let mut assets = vec![]; 340 | for platform_dir in &project.platform_directories { 341 | // only a subset of platforms are supported in pip 342 | match (&platform_dir.os, &platform_dir.cpu) { 343 | (Os::Macos, Cpu::X86_64) 344 | | (Os::Macos, Cpu::Aarch64) 345 | | (Os::Linux, Cpu::X86_64) 346 | | (Os::Linux, Cpu::Aarch64) 347 | | (Os::Windows, Cpu::X86_64) => (), 348 | //(Os::Linux, Cpu::Aarch64) => todo!(), 349 | //(Os::Windows, Cpu::Aarch64) => todo!(), 350 | _ => continue, 351 | } 352 | let mut pkg = PipPackage::new(&project.spec.package.name, &project.version); 353 | assert!(!platform_dir.loadable_files.is_empty()); 354 | let entrypoint = &platform_dir.loadable_files.first().expect("TODO").file_stem; 355 | let mut init_py = templates::base_init_py(&pkg, entrypoint); 356 | if let Some(extra_init_py) = project 357 | .spec 358 | .targets 359 | .pip 360 | .as_ref() 361 | .and_then(|pip| pip.extra_init_py.as_deref()) 362 | { 363 | let contents = std::fs::read_to_string(project.spec_directory.join(extra_init_py))?; 364 | init_py += &contents; 365 | } 366 | pkg.write_library_file("__init__.py", init_py.as_bytes())?; 367 | 368 | for f in &platform_dir.loadable_files { 369 | pkg.write_library_file(f.file.name.as_str(), &f.file.data)?; 370 | } 371 | let platform = Some((&platform_dir.os, &platform_dir.cpu)); 372 | let wheel_name = pkg.wheel_name(platform); 373 | let result = pkg.end(platform)?.into_inner(); 374 | let wheel_path = pip_path.join(wheel_name); 375 | assets.push(GeneratedAsset::from( 376 | GeneratedAssetKind::Pip(AssetPipWheel::Standard(( 377 | platform_dir.os.clone(), 378 | platform_dir.cpu.clone(), 379 | ))), 380 | &wheel_path, 381 | &result, 382 | )?); 383 | } 384 | Ok(assets) 385 | } 386 | 387 | pub(crate) fn write_datasette( 388 | project: &Project, 389 | datasette_path: &Path, 390 | ) -> Result { 391 | let datasette_package_name = format!("datasette-{}", project.spec.package.name); 392 | let dep_pkg = PipPackage::new(&project.spec.package.name, &project.version); 393 | let mut pkg = PipPackage::new(datasette_package_name.clone(), &project.version); 394 | pkg.write_library_file( 395 | "__init__.py", 396 | templates::datasette_init_py(&dep_pkg).as_bytes(), 397 | )?; 398 | 399 | pkg.add_entrypoint( 400 | "datasette", 401 | format!( 402 | "{} = {}", 403 | dep_pkg.python_package_name, pkg.python_package_name 404 | ) 405 | .as_str(), 406 | ); 407 | pkg.extra_metadata 408 | .push(("Requires-Dist".to_owned(), "datasette".to_owned())); 409 | pkg.extra_metadata.push(( 410 | "Requires-Dist".to_owned(), 411 | format!("{} (=={})", &project.spec.package.name, &project.version), 412 | )); 413 | 414 | let wheel_name = pkg.wheel_name(None); 415 | let result = pkg.end(None)?.into_inner(); 416 | Ok(GeneratedAsset::from( 417 | GeneratedAssetKind::Datasette, 418 | &datasette_path.join(wheel_name), 419 | &result, 420 | )?) 421 | } 422 | 423 | pub(crate) fn write_sqlite_utils( 424 | project: &Project, 425 | sqlite_utils_path: &Path, 426 | ) -> Result { 427 | let sqlite_utils_name = format!("sqlite-utils-{}", project.spec.package.name); 428 | let dep_pkg = PipPackage::new(&project.spec.package.name, &project.version); 429 | let mut pkg = PipPackage::new(sqlite_utils_name.clone(), &project.version); 430 | pkg.write_library_file( 431 | "__init__.py", 432 | templates::sqlite_utils_init_py(&dep_pkg).as_bytes(), 433 | )?; 434 | 435 | pkg.add_entrypoint( 436 | "sqlite_utils", 437 | format!( 438 | "{} = {}", 439 | dep_pkg.python_package_name, pkg.python_package_name 440 | ) 441 | .as_str(), 442 | ); 443 | 444 | pkg.extra_metadata 445 | .push(("Requires-Dist".to_owned(), "sqlite-utils".to_owned())); 446 | pkg.extra_metadata.push(( 447 | "Requires-Dist".to_owned(), 448 | format!("{} (=={})", &project.spec.package.name, &project.version), 449 | )); 450 | 451 | let wheel_name = pkg.wheel_name(None); 452 | 453 | let result = pkg.end(None)?.into_inner(); 454 | Ok(GeneratedAsset::from( 455 | GeneratedAssetKind::SqliteUtils, 456 | &sqlite_utils_path.join(wheel_name), 457 | &result, 458 | )?) 459 | } 460 | -------------------------------------------------------------------------------- /src/targets/npm.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | fs, io, 4 | path::{Path, PathBuf}, 5 | }; 6 | 7 | use serde::{Deserialize, Serialize}; 8 | 9 | #[derive(Debug, Deserialize, Serialize)] 10 | pub struct Repository { 11 | #[serde(rename = "type")] 12 | pub repo_type: String, 13 | pub url: String, 14 | pub directory: Option, 15 | } 16 | 17 | #[derive(Debug, Deserialize, Serialize)] 18 | pub struct ExportTarget { 19 | // for CJS, should end in .cjs 20 | #[serde(skip_serializing_if = "Option::is_none")] 21 | pub require: Option, 22 | // for ESM, should end in .mjs 23 | pub import: String, 24 | // for TypeScript, .d.ts file? 25 | #[serde(skip_serializing_if = "Option::is_none")] 26 | pub types: Option, 27 | } 28 | 29 | #[derive(Debug, Deserialize, Serialize)] 30 | pub struct PackageJson { 31 | pub name: String, 32 | pub version: String, 33 | pub author: String, 34 | pub license: String, 35 | pub description: String, 36 | pub repository: Repository, 37 | 38 | // CJS file? 39 | #[serde(skip_serializing_if = "Option::is_none")] 40 | pub main: Option, 41 | // ESM file? 42 | pub module: String, 43 | // path to .d.ts file 44 | #[serde(skip_serializing_if = "Option::is_none")] 45 | pub types: Option, 46 | 47 | pub exports: HashMap, 48 | 49 | #[serde(skip_serializing_if = "Option::is_none")] 50 | pub files: Option>, 51 | 52 | #[serde(skip_serializing_if = "Option::is_none")] 53 | pub keywords: Option>, 54 | 55 | #[serde(skip_serializing_if = "Option::is_none")] 56 | pub dependencies: Option>, 57 | 58 | #[serde(rename = "optionalDependencies")] 59 | #[serde(skip_serializing_if = "Option::is_none")] 60 | pub optional_dependencies: Option>, 61 | 62 | #[serde(rename = "devDependencies")] 63 | #[serde(skip_serializing_if = "Option::is_none")] 64 | pub dev_dependencies: Option>, 65 | 66 | #[serde(skip_serializing_if = "Option::is_none")] 67 | pub os: Option>, 68 | 69 | #[serde(skip_serializing_if = "Option::is_none")] 70 | pub cpu: Option>, 71 | } 72 | 73 | use crate::build::{ 74 | create_targz, Cpu, GeneratedAsset, GeneratedAssetKind, Os, PlatformDirectory, PlatformFile, 75 | Project, 76 | }; 77 | 78 | use thiserror::Error; 79 | 80 | #[derive(Error, Debug)] 81 | pub enum NpmBuildError { 82 | #[error("I/O error: {0}")] 83 | IOError(#[from] io::Error), 84 | #[error("JSON serialization error: {0}")] 85 | JsonError(#[from] serde_json::Error), 86 | } 87 | 88 | struct NpmPlatformPackage { 89 | name: String, 90 | os: Os, 91 | cpu: Cpu, 92 | data: Vec, 93 | } 94 | pub(crate) fn write_npm_packages( 95 | project: &Project, 96 | npm_ouput_directory: &Path, 97 | emscripten_dir: &Option, 98 | ) -> Result, NpmBuildError> { 99 | let mut assets = vec![]; 100 | let author = project.spec.package.authors.first().unwrap(); 101 | let npm_platform_directories: Vec<&PlatformDirectory> = project 102 | .platform_directories 103 | .iter() 104 | .filter(|platform_dir| { 105 | matches!(platform_dir.os, Os::Linux | Os::Macos | Os::Windows) 106 | && matches!(platform_dir.cpu, Cpu::X86_64 | Cpu::Aarch64) 107 | }) 108 | .collect(); 109 | let entrypoint = &npm_platform_directories 110 | .first() 111 | .unwrap() 112 | .loadable_files 113 | .first() 114 | .unwrap() 115 | .file_stem; 116 | 117 | let platform_pkgs: Vec = npm_platform_directories 118 | .iter() 119 | .map(|platform_dir| { 120 | let npm_os = match platform_dir.os { 121 | Os::Linux => "linux", 122 | Os::Macos => "darwin", 123 | Os::Windows => "win32", 124 | _ => unreachable!( 125 | "Invalid npm OS {:?}, should be filtered from above.", 126 | platform_dir.os 127 | ), 128 | }; 129 | let npm_pkg_os_name = match platform_dir.os { 130 | Os::Linux => "linux", 131 | Os::Macos => "darwin", 132 | Os::Windows => "windows", 133 | _ => unreachable!( 134 | "Invalid npm OS {:?}, should be filtered from above.", 135 | platform_dir.os 136 | ), 137 | }; 138 | let npm_cpu = match platform_dir.cpu { 139 | Cpu::X86_64 => "x64", 140 | Cpu::Aarch64 => "arm64", 141 | _ => unreachable!( 142 | "Invalid npm CPU {:?} should be filtered from above.", 143 | platform_dir.cpu 144 | ), 145 | }; 146 | PackageJson { 147 | name: format!( 148 | "{pkg}-{os}-{cpu}", 149 | pkg = project.spec.package.name, 150 | os = npm_pkg_os_name, 151 | cpu = npm_cpu 152 | ), 153 | version: project.version.to_string(), 154 | author: author.clone(), 155 | license: project.spec.package.license.clone(), 156 | description: project.spec.package.description.clone(), 157 | repository: Repository { 158 | repo_type: "git".to_owned(), 159 | url: "https://TODO".to_owned(), 160 | directory: None, 161 | }, 162 | main: Some("./index.cjs".to_owned()), 163 | module: "./index.mjs".to_owned(), 164 | types: Some("./index.d.ts".to_owned()), 165 | exports: HashMap::from([( 166 | ".".to_owned(), 167 | ExportTarget { 168 | require: Some("./index.cjs".to_owned()), 169 | import: "./index.mjs".to_owned(), 170 | types: Some("./index.d.ts".to_owned()), 171 | }, 172 | )]), 173 | files: vec![].into(), 174 | keywords: vec![].into(), 175 | dependencies: None, 176 | optional_dependencies: None, 177 | dev_dependencies: None, 178 | os: Some(vec![npm_os.to_owned()]), 179 | cpu: Some(vec![npm_cpu.to_owned()]), 180 | } 181 | }) 182 | .collect(); 183 | 184 | let pkg_targzs: Result, NpmBuildError> = platform_pkgs 185 | .iter() 186 | .zip(&npm_platform_directories) 187 | .map(|(pkg, platform_dir)| { 188 | let mut files = vec![ 189 | PlatformFile::new("package/README.md", "TODO", None), 190 | PlatformFile::new("package/package.json", serde_json::to_string(&pkg)?, None), 191 | ]; 192 | for loadable_file in &platform_dir.loadable_files { 193 | files.push(PlatformFile::new( 194 | format!("package/{}", loadable_file.file.name), 195 | loadable_file.file.data.clone(), 196 | loadable_file.file.metadata.clone(), 197 | )); 198 | } 199 | 200 | Ok(NpmPlatformPackage { 201 | name: pkg.name.clone(), 202 | os: platform_dir.os.clone(), 203 | cpu: platform_dir.cpu.clone(), 204 | data: create_targz(&files.iter().collect::>())?, 205 | }) 206 | }) 207 | .collect(); 208 | let pkg_targzs = pkg_targzs?; 209 | 210 | let top_pkg = PackageJson { 211 | name: project.spec.package.name.clone(), 212 | version: project.version.to_string(), 213 | author: author.clone(), 214 | license: project.spec.package.license.clone(), 215 | description: project.spec.package.description.clone(), 216 | repository: Repository { 217 | repo_type: "git".to_owned(), 218 | url: "https://TODO".to_owned(), 219 | directory: None, 220 | }, 221 | main: Some("./index.cjs".to_owned()), 222 | module: "./index.mjs".to_owned(), 223 | types: Some("./index.d.ts".to_owned()), 224 | exports: HashMap::from([( 225 | ".".to_owned(), 226 | ExportTarget { 227 | require: Some("./index.cjs".to_owned()), 228 | import: "./index.mjs".to_owned(), 229 | types: Some("./index.d.ts".to_owned()), 230 | }, 231 | )]), 232 | files: vec![].into(), 233 | keywords: vec![].into(), 234 | dependencies: None, 235 | optional_dependencies: Some(HashMap::from_iter( 236 | platform_pkgs 237 | .iter() 238 | .map(|pkg| (pkg.name.clone(), pkg.version.clone())), 239 | )), 240 | dev_dependencies: None, 241 | os: None, 242 | cpu: None, 243 | }; 244 | 245 | let platforms = npm_platform_directories 246 | .iter() 247 | .map(|pd| (pd.os.clone(), pd.cpu.clone())) 248 | .collect::>(); 249 | let pkg_name = project.spec.package.name.clone(); 250 | let top_pkg_targz_files = vec![ 251 | PlatformFile::new("package/README.md", "TODO", None), 252 | PlatformFile::new( 253 | "package/package.json", 254 | serde_json::to_string(&top_pkg)?, 255 | None, 256 | ), 257 | PlatformFile::new( 258 | "package/index.mjs", 259 | templates::index_js(pkg_name.clone(), entrypoint, &platforms, JsFormat::ESM), 260 | None, 261 | ), 262 | PlatformFile::new( 263 | "package/index.cjs", 264 | templates::index_js(pkg_name.clone(), entrypoint, &platforms, JsFormat::CJS), 265 | None, 266 | ), 267 | PlatformFile::new("package/index.d.ts", templates::index_dts(), None), 268 | ]; 269 | if let Some(emscripten_dir) = emscripten_dir { 270 | let wasm_pkg_json = PackageJson { 271 | name: format!("{}-wasm-demo", project.spec.package.name), 272 | version: project.version.to_string(), 273 | author: author.clone(), 274 | license: project.spec.package.license.clone(), 275 | description: project.spec.package.description.clone(), 276 | repository: Repository { 277 | repo_type: "git".to_owned(), 278 | url: "https://TODO".to_owned(), 279 | directory: None, 280 | }, 281 | main: None, 282 | module: "./sqlite3.mjs".to_owned(), 283 | types: None, 284 | exports: HashMap::from([( 285 | ".".to_owned(), 286 | ExportTarget { 287 | require: None, 288 | import: "./sqlite3.mjs".to_owned(), 289 | types: None, 290 | }, 291 | )]), 292 | files: vec![].into(), 293 | keywords: vec![].into(), 294 | dependencies: None, 295 | optional_dependencies: None, 296 | dev_dependencies: None, 297 | os: None, 298 | cpu: None, 299 | }; 300 | let wasm_pkg_targz_files = vec![ 301 | PlatformFile::new("package/README.md", "TODO", None), 302 | PlatformFile::new( 303 | "package/package.json", 304 | serde_json::to_string(&wasm_pkg_json)?, 305 | None, 306 | ), 307 | PlatformFile::new( 308 | "package/sqlite3.mjs", 309 | fs::read(emscripten_dir.join("sqlite3.mjs"))?, 310 | None, 311 | ), 312 | PlatformFile::new( 313 | "package/sqlite3.wasm", 314 | fs::read(emscripten_dir.join("sqlite3.wasm"))?, 315 | None, 316 | ), 317 | ]; 318 | let wasm_pkg_targz = 319 | create_targz(&wasm_pkg_targz_files.iter().collect::>())?; 320 | assets.push(GeneratedAsset::from( 321 | GeneratedAssetKind::Npm(None), 322 | &npm_ouput_directory.join(format!("{}.tar.gz", wasm_pkg_json.name)), 323 | &wasm_pkg_targz, 324 | )?); 325 | } 326 | let top_pkg_targz = create_targz(&top_pkg_targz_files.iter().collect::>()); 327 | 328 | for pkg in pkg_targzs { 329 | assets.push(GeneratedAsset::from( 330 | GeneratedAssetKind::Npm(Some((pkg.os.clone(), pkg.cpu.clone()))), 331 | &npm_ouput_directory.join(format!("{}.tar.gz", pkg.name)), 332 | &pkg.data, 333 | )?); 334 | } 335 | assets.push(GeneratedAsset::from( 336 | GeneratedAssetKind::Npm(None), 337 | &npm_ouput_directory.join(format!("{}.tar.gz", top_pkg.name)), 338 | &top_pkg_targz?, 339 | )?); 340 | Ok(assets) 341 | } 342 | 343 | #[allow(clippy::upper_case_acronyms)] 344 | enum JsFormat { 345 | CJS, 346 | ESM, 347 | } 348 | mod templates { 349 | use crate::build::{Cpu, Os}; 350 | 351 | use super::JsFormat; 352 | pub(crate) fn index_dts() -> String { 353 | r#" 354 | 355 | /** 356 | * TODO JSDoc 357 | */ 358 | export declare function getLoadablePath(): string; 359 | 360 | 361 | interface Db { 362 | loadExtension(file: string, entrypoint?: string | undefined): void; 363 | } 364 | 365 | /** 366 | * TODO JSDoc 367 | */ 368 | export declare function load(db: Db): void; 369 | 370 | "# 371 | .to_string() 372 | } 373 | pub(crate) fn index_js( 374 | pkg_name: String, 375 | entrypoint: &str, 376 | supported_platforms: &[(Os, Cpu)], 377 | format: JsFormat, 378 | ) -> String { 379 | let base_package_name = serde_json::to_string(&serde_json::Value::String(pkg_name.clone())) 380 | .expect("String value should always serialize as JSON"); 381 | let entrypoint_base_name = 382 | serde_json::to_string(&serde_json::Value::String(entrypoint.to_owned())) 383 | .expect("String value should always serialize as JSON"); 384 | 385 | let supported_platforms: Vec> = supported_platforms 386 | .iter() 387 | .map(|(os, cpu)| vec![os.to_string(), cpu.to_string()]) 388 | .collect(); 389 | let supported_platforms = serde_json::to_string(&supported_platforms) 390 | .expect("String values should always serialize as JSON"); 391 | 392 | let imports = match format { 393 | JsFormat::CJS => { 394 | r#" 395 | const { join } = require("node:path"); 396 | const { fileURLToPath } = require("node:url"); 397 | const { arch, platform } = require("node:process"); 398 | const { statSync } = require("node:fs"); 399 | "# 400 | } 401 | JsFormat::ESM => { 402 | r#" 403 | import { join } from "node:path"; 404 | import { fileURLToPath } from "node:url"; 405 | import { arch, platform } from "node:process"; 406 | import { statSync } from "node:fs"; 407 | "# 408 | } 409 | }; 410 | 411 | let exports = match format { 412 | JsFormat::CJS => r#"module.exports = {getLoadablePath, load};"#, 413 | JsFormat::ESM => r#"export {getLoadablePath, load};"#, 414 | }; 415 | let current_directory = match format { 416 | JsFormat::CJS => r#"__dirname"#, 417 | // the "join()" is needed because webpack likes to re-write this sometimes??? unbelievable 418 | JsFormat::ESM => r#"fileURLToPath(new URL(join("."), import.meta.url))"#, 419 | }; 420 | 421 | format!( 422 | r#" 423 | {imports} 424 | 425 | const BASE_PACKAGE_NAME = {base_package_name}; 426 | const ENTRYPOINT_BASE_NAME = {entrypoint_base_name}; 427 | const supportedPlatforms = {supported_platforms}; 428 | 429 | const invalidPlatformErrorMessage = `Unsupported platform for ${{BASE_PACKAGE_NAME}}, on a ${{platform}}-${{arch}} machine. Supported platforms are (${{supportedPlatforms 430 | .map(([p, a]) => `${{p}}-${{a}}`) 431 | .join(",")}}). Consult the ${{BASE_PACKAGE_NAME}} NPM package README for details.`; 432 | 433 | const extensionNotFoundErrorMessage = packageName => `Loadble extension for ${{BASE_PACKAGE_NAME}} not found. Was the ${{packageName}} package installed?`; 434 | 435 | function validPlatform(platform, arch) {{ 436 | return ( 437 | supportedPlatforms.find(([p, a]) => platform == p && arch === a) !== null 438 | ); 439 | }} 440 | function extensionSuffix(platform) {{ 441 | if (platform === "win32") return "dll"; 442 | if (platform === "darwin") return "dylib"; 443 | return "so"; 444 | }} 445 | function platformPackageName(platform, arch) {{ 446 | const os = platform === "win32" ? "windows" : platform; 447 | return `${{BASE_PACKAGE_NAME}}-${{os}}-${{arch}}`; 448 | }} 449 | 450 | function getLoadablePath() {{ 451 | if (!validPlatform(platform, arch)) {{ 452 | throw new Error( 453 | invalidPlatformErrorMessage 454 | ); 455 | }} 456 | const packageName = platformPackageName(platform, arch); 457 | const loadablePath = join( 458 | {current_directory}, 459 | "..", 460 | packageName, 461 | `${{ENTRYPOINT_BASE_NAME}}.${{extensionSuffix(platform)}}` 462 | ); 463 | if (!statSync(loadablePath, {{ throwIfNoEntry: false }})) {{ 464 | throw new Error(extensionNotFoundErrorMessage(packageName)); 465 | }} 466 | 467 | return loadablePath; 468 | }} 469 | 470 | function load(db) {{ 471 | db.loadExtension(getLoadablePath()); 472 | }} 473 | 474 | {exports} 475 | "# 476 | ) 477 | } 478 | } 479 | -------------------------------------------------------------------------------- /src/build.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::BuildCommand; 2 | use crate::targets::{gh_releases, pip, npm, gem, sqlpkg, spm, amalgamation, installer_sh}; 3 | use clap::builder::OsStr; 4 | use flate2::write::GzEncoder; 5 | use flate2::Compression; 6 | use crate::targets::{npm::NpmBuildError, pip::PipBuildError, manifest::write_manifest}; 7 | use semver::Version; 8 | use serde::{Serialize, Serializer}; 9 | use sha2::{Digest, Sha256}; 10 | use crate::spec::Spec; 11 | use std::{ 12 | fs::{self, File}, 13 | io::{self, Write}, 14 | path::PathBuf, 15 | }; 16 | use tar::Header; 17 | 18 | pub struct Project { 19 | pub version: Version, 20 | pub spec: Spec, 21 | pub spec_directory: PathBuf, 22 | pub platform_directories: Vec, 23 | } 24 | 25 | impl Project { 26 | pub(crate) fn release_download_url(&self, name: &str) -> String { 27 | let gh_base = self.spec.package.repo.clone(); 28 | format!( 29 | "{gh_base}/releases/download/{}/{name}", 30 | self.spec.package.git_tag(&self.version) 31 | ) 32 | } 33 | } 34 | 35 | #[derive(Debug, Clone)] 36 | pub struct PlatformDirectory { 37 | pub os: Os, 38 | pub cpu: Cpu, 39 | pub _path: PathBuf, 40 | pub loadable_files: Vec, 41 | pub static_files: Vec, 42 | pub header_files: Vec, 43 | } 44 | 45 | #[derive(Debug, Clone)] 46 | pub enum Os { 47 | Macos, 48 | Linux, 49 | Windows, 50 | Android, 51 | Ios, 52 | IosSimulator, 53 | } 54 | 55 | impl Serialize for Os { 56 | fn serialize(&self, serializer: S) -> Result 57 | where 58 | S: Serializer, 59 | { 60 | serializer.serialize_str(self.to_string().as_str()) 61 | } 62 | } 63 | 64 | impl ToString for Os { 65 | fn to_string(&self) -> String { 66 | match self { 67 | Os::Macos => "macos".to_owned(), 68 | Os::Linux => "linux".to_owned(), 69 | Os::Windows => "windows".to_owned(), 70 | Os::Android => "android".to_owned(), 71 | Os::Ios => "ios".to_owned(), 72 | Os::IosSimulator => "iossimulator".to_owned(), 73 | } 74 | } 75 | } 76 | 77 | #[derive(Debug, Clone)] 78 | pub enum Cpu { 79 | X86_64, 80 | Aarch64, 81 | I686, 82 | Armv7a, 83 | } 84 | 85 | impl Serialize for Cpu { 86 | fn serialize(&self, serializer: S) -> Result 87 | where 88 | S: Serializer, 89 | { 90 | serializer.serialize_str(self.to_string().as_str()) 91 | } 92 | } 93 | 94 | impl ToString for Cpu { 95 | fn to_string(&self) -> String { 96 | match self { 97 | Cpu::X86_64 => "x86_64".to_owned(), 98 | Cpu::Aarch64 => "aarch64".to_owned(), 99 | Cpu::I686 => "i686".to_owned(), 100 | Cpu::Armv7a => "armv7a".to_owned(), 101 | } 102 | } 103 | } 104 | 105 | #[derive(Debug, Clone)] 106 | pub struct GithubRelease { 107 | pub url: String, 108 | pub platform: (Os, Cpu), 109 | } 110 | 111 | #[derive(Debug, Clone)] 112 | pub enum AssetPipWheel { 113 | Standard((Os, Cpu)), 114 | Pyodide, 115 | } 116 | 117 | #[derive(Debug, Clone)] 118 | pub enum GeneratedAssetKind { 119 | Npm(Option<(Os, Cpu)>), 120 | Gem((Os, Cpu)), 121 | Pip(AssetPipWheel), 122 | Datasette, 123 | SqliteUtils, 124 | GithubReleaseLoadable(GithubRelease), 125 | GithubReleaseStatic(GithubRelease), 126 | Sqlpkg, 127 | Spm, 128 | Amalgamation, 129 | Manifest, 130 | } 131 | 132 | impl ToString for GeneratedAssetKind { 133 | fn to_string(&self) -> String { 134 | match self { 135 | GeneratedAssetKind::Npm(_) => "npm".to_owned(), 136 | GeneratedAssetKind::Gem(_) => "gem".to_owned(), 137 | GeneratedAssetKind::Pip(_) => "pip".to_owned(), 138 | GeneratedAssetKind::Datasette => "datasette".to_owned(), 139 | GeneratedAssetKind::SqliteUtils => "sqlite-utils".to_owned(), 140 | GeneratedAssetKind::GithubReleaseLoadable(_) => "github-release-loadable".to_owned(), 141 | GeneratedAssetKind::GithubReleaseStatic(_) => "github-release-static".to_owned(), 142 | GeneratedAssetKind::Sqlpkg => "sqlpkg".to_owned(), 143 | GeneratedAssetKind::Spm => "spm".to_owned(), 144 | GeneratedAssetKind::Amalgamation => "amalgamation".to_owned(), 145 | GeneratedAssetKind::Manifest => "sqlite-dist-manifest".to_owned(), 146 | } 147 | } 148 | } 149 | impl Serialize for GeneratedAssetKind { 150 | fn serialize(&self, serializer: S) -> Result 151 | where 152 | S: Serializer, 153 | { 154 | serializer.serialize_str(self.to_string().as_str()) 155 | } 156 | } 157 | 158 | #[derive(Serialize)] 159 | pub struct GeneratedAsset { 160 | pub kind: GeneratedAssetKind, 161 | pub name: String, 162 | pub path: String, 163 | pub checksum_sha256: String, 164 | pub size: usize, 165 | } 166 | impl GeneratedAsset { 167 | pub fn from(kind: GeneratedAssetKind, path: &PathBuf, contents: &[u8]) -> io::Result { 168 | File::create(path)?.write_all(contents)?; 169 | Ok(Self { 170 | kind, 171 | name: path.file_name().unwrap().to_str().unwrap().to_string(), 172 | path: path.to_str().unwrap().to_string(), 173 | checksum_sha256: base16ct::lower::encode_string(&Sha256::digest(contents)), 174 | size: contents.len(), 175 | }) 176 | } 177 | } 178 | //{"kind": "github_release", "name": "...", "path": "./", "checksum_sha256": ""}, 179 | 180 | #[derive(Debug, Clone)] 181 | pub struct PlatformFile { 182 | pub name: String, 183 | pub data: Vec, 184 | pub metadata: Option, 185 | } 186 | 187 | #[derive(Debug, Clone)] 188 | pub struct LoadablePlatformFile { 189 | pub file_stem: String, 190 | pub file: PlatformFile, 191 | } 192 | 193 | impl PlatformFile { 194 | pub fn new, D: Into>>( 195 | name: S, 196 | data: D, 197 | metadata: Option, 198 | ) -> Self { 199 | Self { 200 | name: name.into(), 201 | data: data.into(), 202 | metadata, 203 | } 204 | } 205 | } 206 | 207 | use thiserror::Error; 208 | 209 | pub fn create_targz(files: &[&PlatformFile]) -> io::Result> { 210 | let mut tar_gz = Vec::new(); 211 | { 212 | let enc = GzEncoder::new(&mut tar_gz, Compression::default()); 213 | let mut tar = tar::Builder::new(enc); 214 | for file in files { 215 | let mut header = Header::new_gnu(); 216 | header.set_path(file.name.clone())?; 217 | header.set_size(file.data.len() as u64); 218 | if let Some(metadata) = &file.metadata { 219 | header.set_metadata(metadata); 220 | } else { 221 | header.set_mode(0o700); 222 | header.set_mtime( 223 | std::time::SystemTime::now() 224 | .duration_since(std::time::UNIX_EPOCH) 225 | .unwrap() 226 | .as_secs(), 227 | ); 228 | } 229 | header.set_cksum(); 230 | tar.append::<&[u8]>(&header, file.data.as_ref())?; 231 | } 232 | tar.finish()?; 233 | }; 234 | Ok(tar_gz) 235 | } 236 | 237 | #[derive(Error, Debug)] 238 | pub enum PlatformDirectoryError { 239 | #[error("I/O error: {0}")] 240 | IOError(#[from] io::Error), 241 | 242 | #[error("Expected name of directory")] 243 | MissingDirectoryName, 244 | #[error("directory or file name must contains only valid UTF-8 characters")] 245 | InvalidCharacters, 246 | #[error("directory {0} is not a valid platform directory. The format must be $OS-$CPU.")] 247 | InvalidDirectoryName(String), 248 | #[error("Invalid operating system '{0}'. Must be one of 'macos', 'linux', or 'windows'")] 249 | InvalidOsValue(String), 250 | #[error("Invalid CPU name '{0}'. Must be one of 'x86_64' or 'aarch64'")] 251 | InvalidCpuValue(String), 252 | } 253 | 254 | impl PlatformDirectory { 255 | fn from_path(base_path: PathBuf) -> Result { 256 | let mut loadable_files = vec![]; 257 | let mut static_files = vec![]; 258 | let mut header_files = vec![]; 259 | 260 | let dirname = base_path 261 | .components() 262 | .last() 263 | .ok_or(PlatformDirectoryError::MissingDirectoryName)? 264 | .as_os_str() 265 | .to_str() 266 | .ok_or(PlatformDirectoryError::InvalidCharacters)?; 267 | let mut s = dirname.split('-'); 268 | let os = match s 269 | .next() 270 | .ok_or_else(|| PlatformDirectoryError::InvalidDirectoryName(dirname.to_owned()))? 271 | { 272 | "macos" => Os::Macos, 273 | "linux" => Os::Linux, 274 | "windows" => Os::Windows, 275 | "android" => Os::Android, 276 | "ios" => Os::Ios, 277 | "iossimulator" => Os::IosSimulator, 278 | os => return Err(PlatformDirectoryError::InvalidOsValue(os.to_owned())), 279 | }; 280 | let cpu = match s 281 | .next() 282 | .ok_or_else(|| PlatformDirectoryError::InvalidDirectoryName(dirname.to_owned()))? 283 | { 284 | "x86_64" => Cpu::X86_64, 285 | "aarch64" => Cpu::Aarch64, 286 | "i686" => Cpu::I686, 287 | "armv7a" => Cpu::Armv7a, 288 | cpu => return Err(PlatformDirectoryError::InvalidCpuValue(cpu.to_owned())), 289 | }; 290 | if s.next().is_some() { 291 | return Err(PlatformDirectoryError::InvalidDirectoryName( 292 | dirname.to_owned(), 293 | )); 294 | } 295 | 296 | let dir = fs::read_dir(&base_path)?; 297 | for entry in dir { 298 | let entry_path = entry?.path(); 299 | match entry_path.extension().and_then(|e| e.to_str()) { 300 | Some("so") | Some("dll") | Some("dylib") => { 301 | let name = entry_path 302 | .file_name() 303 | .expect("file_name to exist because there is an extension") 304 | .to_str() 305 | .ok_or(PlatformDirectoryError::InvalidCharacters)? 306 | .to_string(); 307 | let data = fs::read(&entry_path)?; 308 | let metadata = Some(fs::metadata(&entry_path)?); 309 | let file_stem = entry_path 310 | .file_stem() 311 | .expect("file_stem to exist because there is an extension") 312 | .to_str() 313 | .ok_or(PlatformDirectoryError::InvalidCharacters)? 314 | .to_string(); 315 | loadable_files.push(LoadablePlatformFile { 316 | file_stem, 317 | file: PlatformFile { 318 | name: name.to_string(), 319 | data, 320 | metadata, 321 | }, 322 | }); 323 | } 324 | Some("a") => { 325 | let name = entry_path 326 | .file_name() 327 | .expect("file_name to exist because there is an extension") 328 | .to_str() 329 | .ok_or(PlatformDirectoryError::InvalidCharacters)? 330 | .to_string(); 331 | let data = fs::read(&entry_path)?; 332 | let metadata = Some(fs::metadata(&entry_path)?); 333 | static_files.push(PlatformFile { 334 | name: name.to_string(), 335 | data, 336 | metadata, 337 | }); 338 | } 339 | Some("h") => { 340 | let name = entry_path 341 | .file_name() 342 | .expect("file_name to exist because there is an extension") 343 | .to_str() 344 | .ok_or(PlatformDirectoryError::InvalidCharacters)? 345 | .to_string(); 346 | let data = fs::read(&entry_path)?; 347 | let metadata = Some(fs::metadata(&entry_path)?); 348 | header_files.push(PlatformFile { 349 | name: name.to_string(), 350 | data, 351 | metadata, 352 | }); 353 | } 354 | _ => { 355 | println!("Warning: unknown file type in platform directory"); 356 | } 357 | } 358 | } 359 | Ok(PlatformDirectory { 360 | os, 361 | cpu, 362 | _path: base_path, 363 | loadable_files, 364 | static_files, 365 | header_files, 366 | }) 367 | } 368 | } 369 | 370 | #[derive(Error, Debug)] 371 | pub enum BuildError { 372 | #[error("`{0}` is a required argument")] 373 | RequiredArg(String), 374 | #[error("`{0}` is a required argument")] 375 | InvalidSpec(toml::de::Error), 376 | #[error("specfile error: `{0}`")] 377 | SpecError(String), 378 | #[error("I/O error: {0}")] 379 | IoError(#[from] io::Error), 380 | 381 | #[error("Invalid platform directory: {0}")] 382 | PlayformDirectoryError(#[from] PlatformDirectoryError), 383 | 384 | #[error("Error building a pip package: {0}")] 385 | PipBuildEror(#[from] PipBuildError), 386 | #[error("Error building an npm package: {0}")] 387 | NpmBuildEror(#[from] NpmBuildError), 388 | } 389 | 390 | pub (crate) fn build(args: BuildCommand) -> Result<(), BuildError> { 391 | // Get the values of arguments 392 | let input_dir = args 393 | .input 394 | .ok_or_else(|| BuildError::RequiredArg("input".to_owned()))?; 395 | let output_dir = args 396 | .output 397 | .ok_or_else(|| BuildError::RequiredArg("output".to_owned()))?; 398 | let input_file = &args.file; 399 | let version = Version::parse(&args.set_version).unwrap(); 400 | 401 | std::fs::create_dir_all(&output_dir)?; 402 | 403 | let spec: Spec = match toml::from_str(fs::read_to_string(input_file)?.as_str()) { 404 | Ok(spec) => spec, 405 | Err(err) => { 406 | eprintln!("{}", err); 407 | return Err(BuildError::InvalidSpec(err)); 408 | } 409 | }; 410 | 411 | if spec.targets.sqlpkg.is_some() && spec.targets.github_releases.is_none() { 412 | return Err(BuildError::SpecError( 413 | "sqlpkg target requires the github_releases target".to_owned(), 414 | )); 415 | } 416 | if spec.targets.spm.is_some() && spec.targets.github_releases.is_none() { 417 | return Err(BuildError::SpecError( 418 | "spm target requires the github_releases target".to_owned(), 419 | )); 420 | } 421 | if spec.targets.datasette.is_some() && spec.targets.pip.is_none() { 422 | return Err(BuildError::SpecError( 423 | "datasette target requires the pip target".to_owned(), 424 | )); 425 | } 426 | if spec.targets.sqlite_utils.is_some() && spec.targets.pip.is_none() { 427 | return Err(BuildError::SpecError( 428 | "sqlite_utils target requires the pip target".to_owned(), 429 | )); 430 | } 431 | 432 | let mut entries = fs::read_dir(input_dir)? 433 | .map(|entry| { 434 | Ok(entry 435 | .map_err(|_| { 436 | BuildError::SpecError("Could not read entry in input directory".to_owned()) 437 | })? 438 | .path()) 439 | }) 440 | .collect::, BuildError>>()?; 441 | 442 | let emscripten_dir = entries 443 | .iter() 444 | .position(|entry| entry.file_name() == Some(&OsStr::from("wasm32-emscripten"))) 445 | .map(|item| entries.remove(item)); 446 | 447 | let pyodide_dir = entries 448 | .iter() 449 | .position(|entry| entry.file_name() == Some(&OsStr::from("pyodide"))) 450 | .map(|item| entries.remove(item)); 451 | 452 | let platform_directories: Result, BuildError> = entries 453 | .iter() 454 | .map(|entry| { 455 | PlatformDirectory::from_path(entry.to_owned()) 456 | .map_err(BuildError::PlayformDirectoryError) 457 | }) 458 | .collect(); 459 | let platform_directories = platform_directories?; 460 | 461 | let project = Project { 462 | version, 463 | spec, 464 | spec_directory: input_file.parent().unwrap().to_path_buf(), 465 | platform_directories, 466 | }; 467 | 468 | let mut generated_assets: Vec = vec![]; 469 | if project.spec.targets.github_releases.is_some() { 470 | let path = output_dir.join("github_releases"); 471 | std::fs::create_dir(&path)?; 472 | let gh_release_assets = gh_releases::write_platform_files(&project, &path)?; 473 | 474 | if project.spec.targets.sqlpkg.is_some() { 475 | let sqlpkg_dir = output_dir.join("sqlpkg"); 476 | std::fs::create_dir(&sqlpkg_dir)?; 477 | generated_assets.extend(sqlpkg::write_sqlpkg(&project, &sqlpkg_dir)?); 478 | }; 479 | 480 | if project.spec.targets.spm.is_some() { 481 | let path = output_dir.join("spm"); 482 | std::fs::create_dir(&path)?; 483 | generated_assets.extend(spm::write_spm(&project.spec, &gh_release_assets, &path)?); 484 | }; 485 | 486 | if let Some(amalgamation_config) = &project.spec.targets.amalgamation { 487 | let amalgamation_path = output_dir.join("amalgamation"); 488 | std::fs::create_dir(&amalgamation_path)?; 489 | generated_assets.extend(amalgamation::write_amalgamation( 490 | &project, 491 | &amalgamation_path, 492 | amalgamation_config, 493 | )?); 494 | }; 495 | 496 | generated_assets.extend(gh_release_assets); 497 | }; 498 | 499 | if project.spec.targets.pip.is_some() { 500 | let pip_path = output_dir.join("pip"); 501 | std::fs::create_dir(&pip_path)?; 502 | generated_assets.extend(pip::write_base_packages(&project, &pip_path)?); 503 | if project.spec.targets.datasette.is_some() { 504 | let datasette_path = output_dir.join("datasette"); 505 | std::fs::create_dir(&datasette_path)?; 506 | generated_assets.push(pip::write_datasette(&project, &datasette_path)?); 507 | } 508 | if project.spec.targets.sqlite_utils.is_some() { 509 | let sqlite_utils_path = output_dir.join("sqlite_utils"); 510 | std::fs::create_dir(&sqlite_utils_path)?; 511 | generated_assets.push(pip::write_sqlite_utils(&project, &sqlite_utils_path)?); 512 | } 513 | }; 514 | if project.spec.targets.npm.is_some() { 515 | let npm_output_directory = output_dir.join("npm"); 516 | std::fs::create_dir(&npm_output_directory)?; 517 | generated_assets.extend(npm::write_npm_packages( 518 | &project, 519 | &npm_output_directory, 520 | &emscripten_dir, 521 | )?); 522 | }; 523 | if let Some(gem_config) = &project.spec.targets.gem { 524 | let gem_path = output_dir.join("gem"); 525 | std::fs::create_dir(&gem_path)?; 526 | generated_assets.extend(gem::write_gems(&project, &gem_path, gem_config)?); 527 | }; 528 | 529 | let github_releases_checksums_txt = generated_assets 530 | .iter() 531 | .filter(|ga| { 532 | matches!( 533 | ga.kind, 534 | GeneratedAssetKind::GithubReleaseLoadable(_) 535 | | GeneratedAssetKind::GithubReleaseStatic(_) 536 | | GeneratedAssetKind::Sqlpkg 537 | | GeneratedAssetKind::Spm 538 | ) 539 | }) 540 | .map(|ga| format!("{} {}", ga.name, ga.checksum_sha256)) 541 | .collect::>() 542 | .join("\n"); 543 | File::create(output_dir.join("checksums.txt"))? 544 | .write_all(github_releases_checksums_txt.as_bytes())?; 545 | File::create(output_dir.join("install.sh"))?.write_all( 546 | installer_sh::templates::install_sh(&project, &generated_assets).as_bytes(), 547 | )?; 548 | write_manifest(&output_dir, &generated_assets)?; 549 | Ok(()) 550 | } 551 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "adler" 7 | version = "1.0.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 10 | 11 | [[package]] 12 | name = "aes" 13 | version = "0.8.4" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" 16 | dependencies = [ 17 | "cfg-if", 18 | "cipher", 19 | "cpufeatures", 20 | ] 21 | 22 | [[package]] 23 | name = "android-tzdata" 24 | version = "0.1.1" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 27 | 28 | [[package]] 29 | name = "android_system_properties" 30 | version = "0.1.5" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 33 | dependencies = [ 34 | "libc", 35 | ] 36 | 37 | [[package]] 38 | name = "anstream" 39 | version = "0.6.12" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "96b09b5178381e0874812a9b157f7fe84982617e48f71f4e3235482775e5b540" 42 | dependencies = [ 43 | "anstyle", 44 | "anstyle-parse", 45 | "anstyle-query", 46 | "anstyle-wincon", 47 | "colorchoice", 48 | "utf8parse", 49 | ] 50 | 51 | [[package]] 52 | name = "anstyle" 53 | version = "1.0.6" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" 56 | 57 | [[package]] 58 | name = "anstyle-parse" 59 | version = "0.2.3" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" 62 | dependencies = [ 63 | "utf8parse", 64 | ] 65 | 66 | [[package]] 67 | name = "anstyle-query" 68 | version = "1.0.2" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" 71 | dependencies = [ 72 | "windows-sys", 73 | ] 74 | 75 | [[package]] 76 | name = "anstyle-wincon" 77 | version = "3.0.2" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" 80 | dependencies = [ 81 | "anstyle", 82 | "windows-sys", 83 | ] 84 | 85 | [[package]] 86 | name = "autocfg" 87 | version = "1.1.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 90 | 91 | [[package]] 92 | name = "base16ct" 93 | version = "0.2.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" 96 | 97 | [[package]] 98 | name = "base64" 99 | version = "0.21.7" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 102 | 103 | [[package]] 104 | name = "base64ct" 105 | version = "1.6.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" 108 | 109 | [[package]] 110 | name = "bitflags" 111 | version = "1.3.2" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 114 | 115 | [[package]] 116 | name = "bitflags" 117 | version = "2.4.2" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" 120 | 121 | [[package]] 122 | name = "block-buffer" 123 | version = "0.10.4" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 126 | dependencies = [ 127 | "generic-array", 128 | ] 129 | 130 | [[package]] 131 | name = "bumpalo" 132 | version = "3.15.3" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" 135 | 136 | [[package]] 137 | name = "byteorder" 138 | version = "1.5.0" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 141 | 142 | [[package]] 143 | name = "bzip2" 144 | version = "0.4.4" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" 147 | dependencies = [ 148 | "bzip2-sys", 149 | "libc", 150 | ] 151 | 152 | [[package]] 153 | name = "bzip2-sys" 154 | version = "0.1.11+1.0.8" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" 157 | dependencies = [ 158 | "cc", 159 | "libc", 160 | "pkg-config", 161 | ] 162 | 163 | [[package]] 164 | name = "cc" 165 | version = "1.0.88" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "02f341c093d19155a6e41631ce5971aac4e9a868262212153124c15fa22d1cdc" 168 | dependencies = [ 169 | "libc", 170 | ] 171 | 172 | [[package]] 173 | name = "cfg-if" 174 | version = "1.0.0" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 177 | 178 | [[package]] 179 | name = "chrono" 180 | version = "0.4.34" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" 183 | dependencies = [ 184 | "android-tzdata", 185 | "iana-time-zone", 186 | "js-sys", 187 | "num-traits", 188 | "wasm-bindgen", 189 | "windows-targets", 190 | ] 191 | 192 | [[package]] 193 | name = "cipher" 194 | version = "0.4.4" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" 197 | dependencies = [ 198 | "crypto-common", 199 | "inout", 200 | ] 201 | 202 | [[package]] 203 | name = "clap" 204 | version = "4.5.1" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" 207 | dependencies = [ 208 | "clap_builder", 209 | "clap_derive", 210 | ] 211 | 212 | [[package]] 213 | name = "clap_builder" 214 | version = "4.5.1" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" 217 | dependencies = [ 218 | "anstream", 219 | "anstyle", 220 | "clap_lex", 221 | "strsim", 222 | ] 223 | 224 | [[package]] 225 | name = "clap_derive" 226 | version = "4.5.0" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" 229 | dependencies = [ 230 | "heck", 231 | "proc-macro2", 232 | "quote", 233 | "syn", 234 | ] 235 | 236 | [[package]] 237 | name = "clap_lex" 238 | version = "0.7.0" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" 241 | 242 | [[package]] 243 | name = "colorchoice" 244 | version = "1.0.0" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 247 | 248 | [[package]] 249 | name = "console" 250 | version = "0.15.8" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" 253 | dependencies = [ 254 | "encode_unicode", 255 | "lazy_static", 256 | "libc", 257 | "windows-sys", 258 | ] 259 | 260 | [[package]] 261 | name = "constant_time_eq" 262 | version = "0.1.5" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" 265 | 266 | [[package]] 267 | name = "core-foundation-sys" 268 | version = "0.8.6" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 271 | 272 | [[package]] 273 | name = "cpufeatures" 274 | version = "0.2.12" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" 277 | dependencies = [ 278 | "libc", 279 | ] 280 | 281 | [[package]] 282 | name = "crc32fast" 283 | version = "1.4.0" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" 286 | dependencies = [ 287 | "cfg-if", 288 | ] 289 | 290 | [[package]] 291 | name = "crossbeam-utils" 292 | version = "0.8.19" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" 295 | 296 | [[package]] 297 | name = "crypto-common" 298 | version = "0.1.6" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 301 | dependencies = [ 302 | "generic-array", 303 | "typenum", 304 | ] 305 | 306 | [[package]] 307 | name = "deranged" 308 | version = "0.3.11" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 311 | dependencies = [ 312 | "powerfmt", 313 | ] 314 | 315 | [[package]] 316 | name = "digest" 317 | version = "0.10.7" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 320 | dependencies = [ 321 | "block-buffer", 322 | "crypto-common", 323 | "subtle", 324 | ] 325 | 326 | [[package]] 327 | name = "encode_unicode" 328 | version = "0.3.6" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 331 | 332 | [[package]] 333 | name = "equivalent" 334 | version = "1.0.1" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 337 | 338 | [[package]] 339 | name = "errno" 340 | version = "0.3.8" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" 343 | dependencies = [ 344 | "libc", 345 | "windows-sys", 346 | ] 347 | 348 | [[package]] 349 | name = "filetime" 350 | version = "0.2.23" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" 353 | dependencies = [ 354 | "cfg-if", 355 | "libc", 356 | "redox_syscall", 357 | "windows-sys", 358 | ] 359 | 360 | [[package]] 361 | name = "flate2" 362 | version = "1.0.28" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" 365 | dependencies = [ 366 | "crc32fast", 367 | "miniz_oxide", 368 | ] 369 | 370 | [[package]] 371 | name = "form_urlencoded" 372 | version = "1.2.1" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 375 | dependencies = [ 376 | "percent-encoding", 377 | ] 378 | 379 | [[package]] 380 | name = "fuchsia-cprng" 381 | version = "0.1.1" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" 384 | 385 | [[package]] 386 | name = "generic-array" 387 | version = "0.14.7" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 390 | dependencies = [ 391 | "typenum", 392 | "version_check", 393 | ] 394 | 395 | [[package]] 396 | name = "getrandom" 397 | version = "0.2.12" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" 400 | dependencies = [ 401 | "cfg-if", 402 | "libc", 403 | "wasi", 404 | ] 405 | 406 | [[package]] 407 | name = "hashbrown" 408 | version = "0.14.3" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 411 | 412 | [[package]] 413 | name = "heck" 414 | version = "0.4.1" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 417 | 418 | [[package]] 419 | name = "hmac" 420 | version = "0.12.1" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 423 | dependencies = [ 424 | "digest", 425 | ] 426 | 427 | [[package]] 428 | name = "iana-time-zone" 429 | version = "0.1.60" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" 432 | dependencies = [ 433 | "android_system_properties", 434 | "core-foundation-sys", 435 | "iana-time-zone-haiku", 436 | "js-sys", 437 | "wasm-bindgen", 438 | "windows-core", 439 | ] 440 | 441 | [[package]] 442 | name = "iana-time-zone-haiku" 443 | version = "0.1.2" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 446 | dependencies = [ 447 | "cc", 448 | ] 449 | 450 | [[package]] 451 | name = "idna" 452 | version = "0.5.0" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 455 | dependencies = [ 456 | "unicode-bidi", 457 | "unicode-normalization", 458 | ] 459 | 460 | [[package]] 461 | name = "indexmap" 462 | version = "2.2.3" 463 | source = "registry+https://github.com/rust-lang/crates.io-index" 464 | checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" 465 | dependencies = [ 466 | "equivalent", 467 | "hashbrown", 468 | ] 469 | 470 | [[package]] 471 | name = "inout" 472 | version = "0.1.3" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" 475 | dependencies = [ 476 | "generic-array", 477 | ] 478 | 479 | [[package]] 480 | name = "insta" 481 | version = "1.41.1" 482 | source = "registry+https://github.com/rust-lang/crates.io-index" 483 | checksum = "7e9ffc4d4892617c50a928c52b2961cb5174b6fc6ebf252b2fac9d21955c48b8" 484 | dependencies = [ 485 | "console", 486 | "lazy_static", 487 | "linked-hash-map", 488 | "serde", 489 | "similar", 490 | ] 491 | 492 | [[package]] 493 | name = "insta-cmd" 494 | version = "0.6.0" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "ffeeefa927925cced49ccb01bf3e57c9d4cd132df21e576eb9415baeab2d3de6" 497 | dependencies = [ 498 | "insta", 499 | "serde", 500 | "serde_json", 501 | ] 502 | 503 | [[package]] 504 | name = "itoa" 505 | version = "1.0.10" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 508 | 509 | [[package]] 510 | name = "js-sys" 511 | version = "0.3.68" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" 514 | dependencies = [ 515 | "wasm-bindgen", 516 | ] 517 | 518 | [[package]] 519 | name = "lazy_static" 520 | version = "1.5.0" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 523 | 524 | [[package]] 525 | name = "libc" 526 | version = "0.2.153" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 529 | 530 | [[package]] 531 | name = "linked-hash-map" 532 | version = "0.5.6" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" 535 | 536 | [[package]] 537 | name = "linux-raw-sys" 538 | version = "0.4.13" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" 541 | 542 | [[package]] 543 | name = "log" 544 | version = "0.4.20" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 547 | 548 | [[package]] 549 | name = "memchr" 550 | version = "2.7.1" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 553 | 554 | [[package]] 555 | name = "miniz_oxide" 556 | version = "0.7.2" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" 559 | dependencies = [ 560 | "adler", 561 | ] 562 | 563 | [[package]] 564 | name = "num-conv" 565 | version = "0.1.0" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 568 | 569 | [[package]] 570 | name = "num-traits" 571 | version = "0.2.18" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" 574 | dependencies = [ 575 | "autocfg", 576 | ] 577 | 578 | [[package]] 579 | name = "once_cell" 580 | version = "1.19.0" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 583 | 584 | [[package]] 585 | name = "password-hash" 586 | version = "0.4.2" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" 589 | dependencies = [ 590 | "base64ct", 591 | "rand_core 0.6.4", 592 | "subtle", 593 | ] 594 | 595 | [[package]] 596 | name = "pbkdf2" 597 | version = "0.11.0" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" 600 | dependencies = [ 601 | "digest", 602 | "hmac", 603 | "password-hash", 604 | "sha2", 605 | ] 606 | 607 | [[package]] 608 | name = "percent-encoding" 609 | version = "2.3.1" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 612 | 613 | [[package]] 614 | name = "pkg-config" 615 | version = "0.3.30" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 618 | 619 | [[package]] 620 | name = "powerfmt" 621 | version = "0.2.0" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 624 | 625 | [[package]] 626 | name = "proc-macro2" 627 | version = "1.0.78" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 630 | dependencies = [ 631 | "unicode-ident", 632 | ] 633 | 634 | [[package]] 635 | name = "quote" 636 | version = "1.0.35" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 639 | dependencies = [ 640 | "proc-macro2", 641 | ] 642 | 643 | [[package]] 644 | name = "rand" 645 | version = "0.4.6" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" 648 | dependencies = [ 649 | "fuchsia-cprng", 650 | "libc", 651 | "rand_core 0.3.1", 652 | "rdrand", 653 | "winapi", 654 | ] 655 | 656 | [[package]] 657 | name = "rand_core" 658 | version = "0.3.1" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" 661 | dependencies = [ 662 | "rand_core 0.4.2", 663 | ] 664 | 665 | [[package]] 666 | name = "rand_core" 667 | version = "0.4.2" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" 670 | 671 | [[package]] 672 | name = "rand_core" 673 | version = "0.6.4" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 676 | 677 | [[package]] 678 | name = "rdrand" 679 | version = "0.4.0" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" 682 | dependencies = [ 683 | "rand_core 0.3.1", 684 | ] 685 | 686 | [[package]] 687 | name = "redox_syscall" 688 | version = "0.4.1" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 691 | dependencies = [ 692 | "bitflags 1.3.2", 693 | ] 694 | 695 | [[package]] 696 | name = "remove_dir_all" 697 | version = "0.5.3" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 700 | dependencies = [ 701 | "winapi", 702 | ] 703 | 704 | [[package]] 705 | name = "ring" 706 | version = "0.17.8" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" 709 | dependencies = [ 710 | "cc", 711 | "cfg-if", 712 | "getrandom", 713 | "libc", 714 | "spin", 715 | "untrusted", 716 | "windows-sys", 717 | ] 718 | 719 | [[package]] 720 | name = "rustix" 721 | version = "0.38.31" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" 724 | dependencies = [ 725 | "bitflags 2.4.2", 726 | "errno", 727 | "libc", 728 | "linux-raw-sys", 729 | "windows-sys", 730 | ] 731 | 732 | [[package]] 733 | name = "rustls" 734 | version = "0.22.2" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" 737 | dependencies = [ 738 | "log", 739 | "ring", 740 | "rustls-pki-types", 741 | "rustls-webpki", 742 | "subtle", 743 | "zeroize", 744 | ] 745 | 746 | [[package]] 747 | name = "rustls-pki-types" 748 | version = "1.3.0" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "048a63e5b3ac996d78d402940b5fa47973d2d080c6c6fffa1d0f19c4445310b7" 751 | 752 | [[package]] 753 | name = "rustls-webpki" 754 | version = "0.102.2" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" 757 | dependencies = [ 758 | "ring", 759 | "rustls-pki-types", 760 | "untrusted", 761 | ] 762 | 763 | [[package]] 764 | name = "ryu" 765 | version = "1.0.17" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" 768 | 769 | [[package]] 770 | name = "semver" 771 | version = "1.0.22" 772 | source = "registry+https://github.com/rust-lang/crates.io-index" 773 | checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" 774 | dependencies = [ 775 | "serde", 776 | ] 777 | 778 | [[package]] 779 | name = "serde" 780 | version = "1.0.197" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" 783 | dependencies = [ 784 | "serde_derive", 785 | ] 786 | 787 | [[package]] 788 | name = "serde_derive" 789 | version = "1.0.197" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" 792 | dependencies = [ 793 | "proc-macro2", 794 | "quote", 795 | "syn", 796 | ] 797 | 798 | [[package]] 799 | name = "serde_json" 800 | version = "1.0.114" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" 803 | dependencies = [ 804 | "itoa", 805 | "ryu", 806 | "serde", 807 | ] 808 | 809 | [[package]] 810 | name = "serde_spanned" 811 | version = "0.6.5" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" 814 | dependencies = [ 815 | "serde", 816 | ] 817 | 818 | [[package]] 819 | name = "sha1" 820 | version = "0.10.6" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 823 | dependencies = [ 824 | "cfg-if", 825 | "cpufeatures", 826 | "digest", 827 | ] 828 | 829 | [[package]] 830 | name = "sha2" 831 | version = "0.10.8" 832 | source = "registry+https://github.com/rust-lang/crates.io-index" 833 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 834 | dependencies = [ 835 | "cfg-if", 836 | "cpufeatures", 837 | "digest", 838 | ] 839 | 840 | [[package]] 841 | name = "similar" 842 | version = "2.6.0" 843 | source = "registry+https://github.com/rust-lang/crates.io-index" 844 | checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" 845 | 846 | [[package]] 847 | name = "spin" 848 | version = "0.9.8" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 851 | 852 | [[package]] 853 | name = "sqlite-dist" 854 | version = "0.0.1-alpha.19" 855 | dependencies = [ 856 | "base16ct", 857 | "base64", 858 | "chrono", 859 | "clap", 860 | "flate2", 861 | "insta", 862 | "insta-cmd", 863 | "semver", 864 | "serde", 865 | "serde_json", 866 | "sha2", 867 | "tar", 868 | "tempdir", 869 | "thiserror", 870 | "toml", 871 | "ureq", 872 | "zip", 873 | ] 874 | 875 | [[package]] 876 | name = "strsim" 877 | version = "0.11.0" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" 880 | 881 | [[package]] 882 | name = "subtle" 883 | version = "2.5.0" 884 | source = "registry+https://github.com/rust-lang/crates.io-index" 885 | checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" 886 | 887 | [[package]] 888 | name = "syn" 889 | version = "2.0.50" 890 | source = "registry+https://github.com/rust-lang/crates.io-index" 891 | checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb" 892 | dependencies = [ 893 | "proc-macro2", 894 | "quote", 895 | "unicode-ident", 896 | ] 897 | 898 | [[package]] 899 | name = "tar" 900 | version = "0.4.40" 901 | source = "registry+https://github.com/rust-lang/crates.io-index" 902 | checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" 903 | dependencies = [ 904 | "filetime", 905 | "libc", 906 | "xattr", 907 | ] 908 | 909 | [[package]] 910 | name = "tempdir" 911 | version = "0.3.7" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" 914 | dependencies = [ 915 | "rand", 916 | "remove_dir_all", 917 | ] 918 | 919 | [[package]] 920 | name = "thiserror" 921 | version = "1.0.57" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" 924 | dependencies = [ 925 | "thiserror-impl", 926 | ] 927 | 928 | [[package]] 929 | name = "thiserror-impl" 930 | version = "1.0.57" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" 933 | dependencies = [ 934 | "proc-macro2", 935 | "quote", 936 | "syn", 937 | ] 938 | 939 | [[package]] 940 | name = "time" 941 | version = "0.3.34" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" 944 | dependencies = [ 945 | "deranged", 946 | "num-conv", 947 | "powerfmt", 948 | "serde", 949 | "time-core", 950 | ] 951 | 952 | [[package]] 953 | name = "time-core" 954 | version = "0.1.2" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 957 | 958 | [[package]] 959 | name = "tinyvec" 960 | version = "1.6.0" 961 | source = "registry+https://github.com/rust-lang/crates.io-index" 962 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 963 | dependencies = [ 964 | "tinyvec_macros", 965 | ] 966 | 967 | [[package]] 968 | name = "tinyvec_macros" 969 | version = "0.1.1" 970 | source = "registry+https://github.com/rust-lang/crates.io-index" 971 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 972 | 973 | [[package]] 974 | name = "toml" 975 | version = "0.8.10" 976 | source = "registry+https://github.com/rust-lang/crates.io-index" 977 | checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" 978 | dependencies = [ 979 | "serde", 980 | "serde_spanned", 981 | "toml_datetime", 982 | "toml_edit", 983 | ] 984 | 985 | [[package]] 986 | name = "toml_datetime" 987 | version = "0.6.5" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" 990 | dependencies = [ 991 | "serde", 992 | ] 993 | 994 | [[package]] 995 | name = "toml_edit" 996 | version = "0.22.6" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" 999 | dependencies = [ 1000 | "indexmap", 1001 | "serde", 1002 | "serde_spanned", 1003 | "toml_datetime", 1004 | "winnow", 1005 | ] 1006 | 1007 | [[package]] 1008 | name = "typenum" 1009 | version = "1.17.0" 1010 | source = "registry+https://github.com/rust-lang/crates.io-index" 1011 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 1012 | 1013 | [[package]] 1014 | name = "unicode-bidi" 1015 | version = "0.3.15" 1016 | source = "registry+https://github.com/rust-lang/crates.io-index" 1017 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 1018 | 1019 | [[package]] 1020 | name = "unicode-ident" 1021 | version = "1.0.12" 1022 | source = "registry+https://github.com/rust-lang/crates.io-index" 1023 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1024 | 1025 | [[package]] 1026 | name = "unicode-normalization" 1027 | version = "0.1.23" 1028 | source = "registry+https://github.com/rust-lang/crates.io-index" 1029 | checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" 1030 | dependencies = [ 1031 | "tinyvec", 1032 | ] 1033 | 1034 | [[package]] 1035 | name = "untrusted" 1036 | version = "0.9.0" 1037 | source = "registry+https://github.com/rust-lang/crates.io-index" 1038 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1039 | 1040 | [[package]] 1041 | name = "ureq" 1042 | version = "2.9.6" 1043 | source = "registry+https://github.com/rust-lang/crates.io-index" 1044 | checksum = "11f214ce18d8b2cbe84ed3aa6486ed3f5b285cf8d8fbdbce9f3f767a724adc35" 1045 | dependencies = [ 1046 | "base64", 1047 | "flate2", 1048 | "log", 1049 | "once_cell", 1050 | "rustls", 1051 | "rustls-pki-types", 1052 | "rustls-webpki", 1053 | "url", 1054 | "webpki-roots", 1055 | ] 1056 | 1057 | [[package]] 1058 | name = "url" 1059 | version = "2.5.0" 1060 | source = "registry+https://github.com/rust-lang/crates.io-index" 1061 | checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" 1062 | dependencies = [ 1063 | "form_urlencoded", 1064 | "idna", 1065 | "percent-encoding", 1066 | ] 1067 | 1068 | [[package]] 1069 | name = "utf8parse" 1070 | version = "0.2.1" 1071 | source = "registry+https://github.com/rust-lang/crates.io-index" 1072 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 1073 | 1074 | [[package]] 1075 | name = "version_check" 1076 | version = "0.9.4" 1077 | source = "registry+https://github.com/rust-lang/crates.io-index" 1078 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1079 | 1080 | [[package]] 1081 | name = "wasi" 1082 | version = "0.11.0+wasi-snapshot-preview1" 1083 | source = "registry+https://github.com/rust-lang/crates.io-index" 1084 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1085 | 1086 | [[package]] 1087 | name = "wasm-bindgen" 1088 | version = "0.2.91" 1089 | source = "registry+https://github.com/rust-lang/crates.io-index" 1090 | checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" 1091 | dependencies = [ 1092 | "cfg-if", 1093 | "wasm-bindgen-macro", 1094 | ] 1095 | 1096 | [[package]] 1097 | name = "wasm-bindgen-backend" 1098 | version = "0.2.91" 1099 | source = "registry+https://github.com/rust-lang/crates.io-index" 1100 | checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" 1101 | dependencies = [ 1102 | "bumpalo", 1103 | "log", 1104 | "once_cell", 1105 | "proc-macro2", 1106 | "quote", 1107 | "syn", 1108 | "wasm-bindgen-shared", 1109 | ] 1110 | 1111 | [[package]] 1112 | name = "wasm-bindgen-macro" 1113 | version = "0.2.91" 1114 | source = "registry+https://github.com/rust-lang/crates.io-index" 1115 | checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" 1116 | dependencies = [ 1117 | "quote", 1118 | "wasm-bindgen-macro-support", 1119 | ] 1120 | 1121 | [[package]] 1122 | name = "wasm-bindgen-macro-support" 1123 | version = "0.2.91" 1124 | source = "registry+https://github.com/rust-lang/crates.io-index" 1125 | checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" 1126 | dependencies = [ 1127 | "proc-macro2", 1128 | "quote", 1129 | "syn", 1130 | "wasm-bindgen-backend", 1131 | "wasm-bindgen-shared", 1132 | ] 1133 | 1134 | [[package]] 1135 | name = "wasm-bindgen-shared" 1136 | version = "0.2.91" 1137 | source = "registry+https://github.com/rust-lang/crates.io-index" 1138 | checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" 1139 | 1140 | [[package]] 1141 | name = "webpki-roots" 1142 | version = "0.26.1" 1143 | source = "registry+https://github.com/rust-lang/crates.io-index" 1144 | checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" 1145 | dependencies = [ 1146 | "rustls-pki-types", 1147 | ] 1148 | 1149 | [[package]] 1150 | name = "winapi" 1151 | version = "0.3.9" 1152 | source = "registry+https://github.com/rust-lang/crates.io-index" 1153 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1154 | dependencies = [ 1155 | "winapi-i686-pc-windows-gnu", 1156 | "winapi-x86_64-pc-windows-gnu", 1157 | ] 1158 | 1159 | [[package]] 1160 | name = "winapi-i686-pc-windows-gnu" 1161 | version = "0.4.0" 1162 | source = "registry+https://github.com/rust-lang/crates.io-index" 1163 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1164 | 1165 | [[package]] 1166 | name = "winapi-x86_64-pc-windows-gnu" 1167 | version = "0.4.0" 1168 | source = "registry+https://github.com/rust-lang/crates.io-index" 1169 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1170 | 1171 | [[package]] 1172 | name = "windows-core" 1173 | version = "0.52.0" 1174 | source = "registry+https://github.com/rust-lang/crates.io-index" 1175 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1176 | dependencies = [ 1177 | "windows-targets", 1178 | ] 1179 | 1180 | [[package]] 1181 | name = "windows-sys" 1182 | version = "0.52.0" 1183 | source = "registry+https://github.com/rust-lang/crates.io-index" 1184 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1185 | dependencies = [ 1186 | "windows-targets", 1187 | ] 1188 | 1189 | [[package]] 1190 | name = "windows-targets" 1191 | version = "0.52.3" 1192 | source = "registry+https://github.com/rust-lang/crates.io-index" 1193 | checksum = "d380ba1dc7187569a8a9e91ed34b8ccfc33123bbacb8c0aed2d1ad7f3ef2dc5f" 1194 | dependencies = [ 1195 | "windows_aarch64_gnullvm", 1196 | "windows_aarch64_msvc", 1197 | "windows_i686_gnu", 1198 | "windows_i686_msvc", 1199 | "windows_x86_64_gnu", 1200 | "windows_x86_64_gnullvm", 1201 | "windows_x86_64_msvc", 1202 | ] 1203 | 1204 | [[package]] 1205 | name = "windows_aarch64_gnullvm" 1206 | version = "0.52.3" 1207 | source = "registry+https://github.com/rust-lang/crates.io-index" 1208 | checksum = "68e5dcfb9413f53afd9c8f86e56a7b4d86d9a2fa26090ea2dc9e40fba56c6ec6" 1209 | 1210 | [[package]] 1211 | name = "windows_aarch64_msvc" 1212 | version = "0.52.3" 1213 | source = "registry+https://github.com/rust-lang/crates.io-index" 1214 | checksum = "8dab469ebbc45798319e69eebf92308e541ce46760b49b18c6b3fe5e8965b30f" 1215 | 1216 | [[package]] 1217 | name = "windows_i686_gnu" 1218 | version = "0.52.3" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "2a4e9b6a7cac734a8b4138a4e1044eac3404d8326b6c0f939276560687a033fb" 1221 | 1222 | [[package]] 1223 | name = "windows_i686_msvc" 1224 | version = "0.52.3" 1225 | source = "registry+https://github.com/rust-lang/crates.io-index" 1226 | checksum = "28b0ec9c422ca95ff34a78755cfa6ad4a51371da2a5ace67500cf7ca5f232c58" 1227 | 1228 | [[package]] 1229 | name = "windows_x86_64_gnu" 1230 | version = "0.52.3" 1231 | source = "registry+https://github.com/rust-lang/crates.io-index" 1232 | checksum = "704131571ba93e89d7cd43482277d6632589b18ecf4468f591fbae0a8b101614" 1233 | 1234 | [[package]] 1235 | name = "windows_x86_64_gnullvm" 1236 | version = "0.52.3" 1237 | source = "registry+https://github.com/rust-lang/crates.io-index" 1238 | checksum = "42079295511643151e98d61c38c0acc444e52dd42ab456f7ccfd5152e8ecf21c" 1239 | 1240 | [[package]] 1241 | name = "windows_x86_64_msvc" 1242 | version = "0.52.3" 1243 | source = "registry+https://github.com/rust-lang/crates.io-index" 1244 | checksum = "0770833d60a970638e989b3fa9fd2bb1aaadcf88963d1659fd7d9990196ed2d6" 1245 | 1246 | [[package]] 1247 | name = "winnow" 1248 | version = "0.6.2" 1249 | source = "registry+https://github.com/rust-lang/crates.io-index" 1250 | checksum = "7a4191c47f15cc3ec71fcb4913cb83d58def65dd3787610213c649283b5ce178" 1251 | dependencies = [ 1252 | "memchr", 1253 | ] 1254 | 1255 | [[package]] 1256 | name = "xattr" 1257 | version = "1.3.1" 1258 | source = "registry+https://github.com/rust-lang/crates.io-index" 1259 | checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" 1260 | dependencies = [ 1261 | "libc", 1262 | "linux-raw-sys", 1263 | "rustix", 1264 | ] 1265 | 1266 | [[package]] 1267 | name = "zeroize" 1268 | version = "1.7.0" 1269 | source = "registry+https://github.com/rust-lang/crates.io-index" 1270 | checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" 1271 | 1272 | [[package]] 1273 | name = "zip" 1274 | version = "0.6.6" 1275 | source = "registry+https://github.com/rust-lang/crates.io-index" 1276 | checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" 1277 | dependencies = [ 1278 | "aes", 1279 | "byteorder", 1280 | "bzip2", 1281 | "constant_time_eq", 1282 | "crc32fast", 1283 | "crossbeam-utils", 1284 | "flate2", 1285 | "hmac", 1286 | "pbkdf2", 1287 | "sha1", 1288 | "time", 1289 | "zstd", 1290 | ] 1291 | 1292 | [[package]] 1293 | name = "zstd" 1294 | version = "0.11.2+zstd.1.5.2" 1295 | source = "registry+https://github.com/rust-lang/crates.io-index" 1296 | checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" 1297 | dependencies = [ 1298 | "zstd-safe", 1299 | ] 1300 | 1301 | [[package]] 1302 | name = "zstd-safe" 1303 | version = "5.0.2+zstd.1.5.2" 1304 | source = "registry+https://github.com/rust-lang/crates.io-index" 1305 | checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" 1306 | dependencies = [ 1307 | "libc", 1308 | "zstd-sys", 1309 | ] 1310 | 1311 | [[package]] 1312 | name = "zstd-sys" 1313 | version = "2.0.9+zstd.1.5.5" 1314 | source = "registry+https://github.com/rust-lang/crates.io-index" 1315 | checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" 1316 | dependencies = [ 1317 | "cc", 1318 | "pkg-config", 1319 | ] 1320 | --------------------------------------------------------------------------------