Project Scaffold

quack_rs::scaffold::generate_scaffold generates every file of a new DuckDB community extension project in Rust — Cargo.toml, Makefile, CI workflow, description.yml, an example function and its tests — from a single function call.


What it generates

my_extension/
├── Cargo.toml                          # cdylib crate, dependencies, release profile
├── Makefile                            # delegates to cargo + extension-ci-tools
├── extension_config.cmake              # required by extension-ci-tools
├── src/
│   ├── lib.rs                          # entry point, example function, unit test
│   └── wasm_lib.rs                     # WASM staticlib shim
├── description.yml                     # community extension metadata
├── test/
│   └── sql/
│       └── my_extension.test           # SQLLogicTest for the example function
├── .github/
│   └── workflows/
│       └── extension-ci.yml            # cross-platform CI workflow
├── .gitmodules                         # extension-ci-tools submodule
├── .gitignore
└── .cargo/
    └── config.toml                     # Windows CRT static linking

Usage

generate_scaffold returns paths relative to the project root (Cargo.toml, src/lib.rs, ...) and writes nothing itself. Join them under the directory the project should live in — writing them relative to the current directory would overwrite whatever Cargo.toml and src/lib.rs are already there.

use quack_rs::scaffold::{ScaffoldConfig, generate_scaffold};
use std::path::Path;

fn main() {
    let config = ScaffoldConfig {
        name: "my_extension".to_string(),
        description: "My DuckDB extension".to_string(),
        version: "0.1.0".to_string(),
        license: "MIT".to_string(),
        maintainer: "Your Name".to_string(),
        github_repo: "yourorg/duckdb-my-extension".to_string(),
        excluded_platforms: vec![],
        // `target_duckdb_version`, `use_unstable_c_api` and `git_ref` default
        // to the stable-ABI settings; see `concepts/abi.md`.
        ..ScaffoldConfig::default()
    };

    let files = generate_scaffold(&config).expect("scaffold generation failed");

    // Everything goes under ./my_extension/, never into the current directory.
    let root = Path::new(&config.name);
    for file in &files {
        let path = root.join(&file.path);
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent).unwrap();
        }
        std::fs::write(&path, &file.content).unwrap();
        println!("created {}", path.display());
    }
}

ScaffoldConfig fields

FieldTypeDescription
nameStringExtension name — must match [lib] name in Cargo.toml and description.yml
descriptionStringDescription for description.yml and the //! docs of src/lib.rs. Quoted and escaped, so :, # and quotes are fine; must not be empty, padded with whitespace, or contain control characters other than newline and tab
versionStringThe extension's version — validated by validate_extension_version
licenseStringSPDX license identifier (e.g., "MIT", "Apache-2.0")
maintainerStringYour name or org, listed in description.yml — one non-empty line
github_repoString"owner/repo", in the characters GitHub allows
excluded_platformsVec<String>Platforms to skip (e.g., ["wasm_mvp", "wasm_eh"])
git_refStringrepo.ref — a commit hash (or tag), not a branch. Defaults to REF_PLACEHOLDER so it cannot be submitted unset
target_duckdb_versionStringWritten as TARGET_DUCKDB_VERSION in the Makefile. Defaults to DUCKDB_API_VERSION (v1.2.0); with use_unstable_c_api, an exact DuckDB release such as v1.5.5
use_unstable_c_apiboolSet when the extension enables duckdb-1-5 / duckdb-1-5-3 / duckdb-1-5-4. Defaults to false

Name validation

Extension names must satisfy all of:

  • Match ^[a-z][a-z0-9_]*$ (no hyphens: the entry point is <name>_init_c_api)
  • Not exceed 64 characters
  • Be globally unique on community-extensions.duckdb.org

Use vendor-prefixed names to avoid collisions: myorg_analytics, not analytics.

The scaffold generator checks the first two rules before generating any files and returns an error if the name breaks one. Uniqueness is yours to check.


After scaffolding

cd my_extension
git init
git submodule add https://github.com/duckdb/extension-ci-tools.git extension-ci-tools
make configure
make release
make test

git submodule add is not optional in a new project: the scaffold writes .gitmodules, but git submodule update --init does nothing until the submodule has been added once (Pitfall P4). The generated Makefile stops with this command if the checkout is missing.

cargo test runs the generated unit test and make test runs test/sql/my_extension.test against a real DuckDB. Replace the example function in src/lib.rs with your own, give each new function a test in both places, and push to GitHub — CI runs automatically.


Excluded platforms

Some extensions cannot be built for all platforms (e.g., extensions that depend on platform-specific system libraries, or WASM environments that lack threading).

#![allow(unused)]
fn main() {
use quack_rs::scaffold::ScaffoldConfig;

let config = ScaffoldConfig {
    excluded_platforms: vec![
        "wasm_mvp".to_string(),
        "wasm_eh".to_string(),
        "wasm_threads".to_string(),
    ],
    ..ScaffoldConfig::default()
};
}

Validate individual platform names with quack_rs::validate::validate_platform, or a semicolon-delimited string (as used in description.yml) with quack_rs::validate::validate_excluded_platforms_str.