Source-only package manager for C source code.
cpkg is a minimal, Go-implemented module and dependency manager for C (and SKC firmware) inspired by Go modules and Elm packages.
- Source-only: no prebuilt binaries, no ABI matrix.
- Git-first: modules are git repos, addressed by URL-ish paths.
- Semver: dependencies are versioned with semantic version tags.
- Build-agnostic: cpkg resolves and lays out source; your build system compiles it.
- Submodule-native: cpkg wraps git submodules to keep repos clean and reviewable.
- Incremental adoption: Use arbitrary subdirectories from any repo without upstream changes.
- Version flexibility: Different subdirectories can use different versions from the same repo.
# Install from source (recommended)
go install github.com/SCKelemen/cpkg@latest
# Or download a pre-built binary from the [Releases](https://github.com/SCKelemen/cpkg/releases) page# Initialize a new module
cpkg init
# Add a dependency
cpkg add github.com/user/repo@^1.0.0
# Resolve and lock dependencies
cpkg tidy
# Sync git submodules
cpkg sync
# Build the project
cpkg build# Add a new dependency with a version constraint
cpkg add github.com/user/repo@^1.0.0
# Add multiple dependencies at once
cpkg add github.com/user/repo1@^1.0.0 github.com/user/repo2@^2.0.0
# Add a module from a subpath (multi-module support)
cpkg add github.com/user/repo/intrusive_list@^1.0.0
cpkg add github.com/user/repo/span@^1.0.0cpkg uses Go-style module discovery: when you run cpkg commands, it automatically finds the nearest cpkg.yaml by walking up the directory tree from your current working directory. This enables:
- Multiple modules per repository: You can have multiple
cpkg.yamlfiles in subdirectories, each treated as an independent module - Independent dependency management: Each module has its own
lock.cpkg.yamland dependency graph - Flexible project structure: Run
cpkgcommands from any subdirectory and it will find the correct module
Example structure:
my-repo/
├── cpkg.yaml # Root module
├── lock.cpkg.yaml
├── libs/
│ ├── span/
│ │ ├── cpkg.yaml # Nested module
│ │ └── lock.cpkg.yaml
│ └── view/
│ ├── cpkg.yaml # Another nested module
│ └── lock.cpkg.yaml
Note: Unlike Go's workspace mode, cpkg commands operate on only one module at a time (the one whose manifest is found). There is no workspace abstraction that manages multiple modules simultaneously.
cpkg supports multiple modules from the same repository, with two modes:
If you control the repository, you can create tags for each submodule:
Tag Naming Conventions:
-
Prefix format (recommended):
subpath/v1.0.0git tag intrusive_list/v1.0.0 git tag span/v1.0.0
-
Suffix format:
v1.0.0-subpathgit tag v1.0.0-intrusive_list git tag v1.0.0-span
Example:
# cpkg.yaml
dependencies:
github.com/user/firmware-lib/intrusive_list:
version: "^1.0.0"
github.com/user/firmware-lib/span:
version: "^1.0.0"You can use any subdirectory from any repository, even if it doesn't have cpkg-specific tags or even know about cpkg. This enables incremental adoption - you can start using parts of a library without requiring the upstream repo to support cpkg.
cpkg will use the root repository tags and point to the subdirectory:
# cpkg.yaml
dependencies:
github.com/Mbed-TLS/mbedtls/library: # Just the library source files
version: "^3.6.0"
github.com/Mbed-TLS/mbedtls/include: # Just the headers
version: "^3.6.0"Different versions for different subdirectories:
You can even use different versions of different subdirectories from the same repo:
# cpkg.yaml
dependencies:
github.com/Mbed-TLS/mbedtls/library: # Use v3.6.4
version: "3.6.4"
github.com/Mbed-TLS/mbedtls/include: # Use v3.6.5
version: "3.6.5"This works because cpkg:
- Uses the root repo tags (e.g.,
v3.6.4,v3.6.5) - Points to the subdirectory (e.g.,
library/orinclude/) - Creates separate submodules for each, allowing different commits
- Each submodule can be at a different commit, even from the same repo
How it works with different commits:
Each module from the same repo gets its own git submodule. This allows independently versioned modules to be at different commits:
intrusive_list/v1.0.0→ commitabc123→ submodule atthird_party/cpkg/.../intrusive_listspan/v1.0.0→ commitdef456→ submodule atthird_party/cpkg/.../span
This is necessary because git submodules can only point to a single commit. See Multiple Commits for details.
# Check which dependencies have updates available
cpkg checkThis will show a table of dependencies with their current version, latest compatible version, and update type (major/minor/patch).
# Upgrade all dependencies to latest compatible versions
cpkg upgrade
# Upgrade all dependencies (even if already up to date)
cpkg upgrade --allThe upgrade command will:
- Check each dependency for newer compatible versions
- Update the lockfile with new versions
- Sync git submodules to the new commits
# List all dependencies
cpkg list
# Get detailed information about a specific dependency
cpkg explain github.com/user/repo
# View dependency graph
cpkg graphIf you prefer to manually update dependencies:
# 1. Check for available updates
cpkg check
# 2. Update the version constraint in cpkg.yaml (optional)
cpkg add github.com/user/repo@^2.0.0
# 3. Resolve and lock new versions
cpkg tidy
# 4. Sync submodules to locked versions
cpkg synccpkg provides a reusable GitHub Action for automated dependency updates. You can use it in two ways:
Add this to your .github/workflows/dependencies.yml:
name: Update Dependencies
on:
schedule:
# Run weekly on Monday at 00:00 UTC
- cron: '0 0 * * 1'
workflow_dispatch:
jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Update dependencies
uses: github.com/SCKelemen/cpkg/.github/actions/cpkg-upgrade@main
with:
token: ${{ secrets.GITHUB_TOKEN }}
pr-title: "chore: update dependencies"
pr-body: |
Automated dependency update.
This PR was created by the cpkg upgrade workflow.If you prefer to use a dedicated action repository (e.g., github.com/SCKelemen/cpkg-upgrade-action), you can reference it directly:
- name: Update dependencies
uses: github.com/SCKelemen/cpkg-upgrade-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}The action will:
- Check for available updates using
cpkg check - Upgrade them to latest compatible versions using
cpkg upgrade - Create a pull request with the changes (only if there are actual updates)
You can customize the PR title, body, and branch name:
- name: Update dependencies
uses: github.com/SCKelemen/cpkg/.github/actions/cpkg-upgrade@main
with:
token: ${{ secrets.GITHUB_TOKEN }}
pr-title: "chore(deps): upgrade dependencies"
pr-body: "Automated dependency updates via cpkg"
branch: update-deps
commit-message: "chore: update dependencies"The lock.cpkg.yaml file is similar to Go's go.mod + go.sum combined:
- Version locking: Pins exact versions and commits (like
go.mod) - Integrity checking: Includes checksums to verify dependency integrity (like
go.sum) - Subdirectory tracking: For multi-module repos, includes a
subdirfield indicating where source files are located
It's a single file for simplicity, serving both purposes.
dependencies:
github.com/user/repo/intrusive_list:
version: v1.0.0
commit: abc123...
repoURL: https://github.com/user/repo.git
path: third_party/cpkg/github.com/user/repo/intrusive_list
subdir: intrusive_list # ← Source files are in this subdirectorycpkg manages dependencies and provides source files, but does not control the compiler or linker. Your build system is responsible for compiling and linking.
The lockfile includes a sourcePath field that points directly to where the source files are:
dependencies:
github.com/user/repo/intrusive_list:
path: third_party/cpkg/github.com/user/repo/intrusive_list # Submodule path
subdir: intrusive_list # Subdirectory within repo
sourcePath: third_party/cpkg/github.com/user/repo/intrusive_list/intrusive_list # ← Use this!Your build system should:
- Read
lock.cpkg.yaml - Use the
sourcePathfield directly (no computation needed) - Add include paths and compile
For a simpler flat structure, use cpkg vendor:
# Default: creates symlinks on Unix (macOS/Linux), copies on Windows
cpkg vendor
# Force symlinks (faster, no duplication)
cpkg vendor --symlink
# Force copying (more compatible, uses more disk space)
cpkg vendor --copySymlinks vs Copying:
- Symlinks (default on Unix): No disk duplication, always in sync with submodules, faster. Best for development on macOS/Linux.
- Copies (default on Windows): More compatible with all build systems, works offline after vendoring.
- Copying (default): Works everywhere, build systems always handle it correctly, but uses more disk space.
Both create a clean vendor/ directory structure that's easy for build systems to use.
See Build System Integration for detailed examples with CMake, Make, etc.
- Full Specification - Complete cpkg specification
- Build System Integration - How to integrate cpkg with your build system
- Multi-Module Support - Using multiple modules from the same repository
- Multiple Commits - How cpkg handles different commits for modules from the same repo
- Vendor Directory - Understanding the vendor command and directory structure
MIT