Skip to content

feat: declare canister dependencies from another icp project#641

Draft
lwshang wants to merge 9 commits into
mainfrom
feat/project-dependencies
Draft

feat: declare canister dependencies from another icp project#641
lwshang wants to merge 9 commits into
mainfrom
feat/project-dependencies

Conversation

@lwshang

@lwshang lwshang commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a top-level dependencies: block to icp.yaml so a project can depend on another icp project vendored into it (typically as a git submodule). icp deploy then deploys the dependency's canisters into the same environment and wires their IDs into your canisters as PUBLIC_CANISTER_ID:<alias>:<canister> environment variables.

# app/icp.yaml
dependencies:
  - name: openemail          # local alias
    path: ./vendor/openemail # dir containing the dependency's icp.yaml
    canisters: [backend]     # which of its canisters to expose (omit for all)

Running icp deploy in app/ builds/creates/installs the app's canisters and the dependency's, then injects e.g. PUBLIC_CANISTER_ID:openemail:backend into the app's canisters. The dependency's icp.yaml stays self-contained and can still be deployed on its own.

Behavior

  • Deploy-all, expose-subset. The whole dependency is always deployed (its canisters may call each other, and icp-cli has no intra-project "requires" graph). canisters: only filters which dependency IDs are exposed to the parent.
  • Per-project env-var views. Each canister sees the view its owning project expects: its own canisters by their local names, plus declared dependencies under their aliases. So a dependency's canisters behave identically whether deployed standalone or as a dependency, and the parent only sees the dependency canisters it selected. (A dependency-free project is unchanged: every canister still sees every sibling.)
  • Shared dependencies de-duplicate by resolved directory. In the "umbrella" layout — sibling submodules service-a, service-b, and openemail, where both services depend on ../openemail — the two edges resolve to the same directory, so openemail is deployed once and shared by both.
  • Store keys are path-based. Because two projects can each define a canister with the same name, imported canisters are keyed by their path relative to the project root (e.g. vendor/openemail:backend). Aliases drive only env-var names. Cycles are detected.

Implementation

Commit-by-commit:

  1. feat(manifest) — the dependencies: manifest type + JSON schema.
  2. feat(project) — recursive consolidation: import-all, path-based store keys, diamond de-dup by canonical directory, cycle detection, per-project PUBLIC_CANISTER_ID bindings, : name/alias validation, and translation of dependency canisters' name-based controller refs.
  3. feat(deploy) — inject env vars from each canister's per-project bindings; percent-encode namespaced names in the artifact store so they are valid filenames.
  4. fix(url) — namespaced canister names fall back to the principal host when printing URLs (not valid DNS labels).
  5. docs(deps)examples/icp-project-dependency, an integration test, canister-discovery docs, CHANGELOG.
  6. test(deps)examples/icp-project-dependency-shared (umbrella layout) + a shared-dependency integration test.

Testing

  • Unit tests for consolidation: single-project flat behavior, import + exposure subset, umbrella diamond → single shared instance, cycle detection, and every validation error.
  • Two integration tests deploy to a real managed network and assert the injected env vars (single-dependency and the shared/umbrella layout, verifying both services resolve openemail:backend to the same shared canister).
  • No regressions across the canister_settings, deploy, bundle, build, and project suites; fmt/clippy clean.

Not in scope (design leaves room for these)

Pinned/already-deployed dependencies (an additive per-network pin:), candid/binding generation (each canister sources its own bindings), and per-canister depends_on.

🤖 Generated with Claude Code

lwshang and others added 9 commits July 8, 2026 21:46
Introduce a top-level `dependencies:` block in icp.yaml for declaring
another icp project this project depends on. Each entry has a local alias
(`name`), a `path` to the dependency's project dir, and an optional
`canisters` exposure subset (omit = expose all).

This commit adds only the manifest type + schema; consolidation and deploy
behavior follow in later commits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ject

Extend consolidate_manifest to import declared dependency projects:

- Recursively load each dependency's icp.yaml and import ALL of its canisters
  (deploy-all), keyed by their app-root-relative path (e.g.
  `umbrella/openemail:backend`). The `canisters` field is an exposure filter for
  env vars only, not a deployment filter.
- De-duplicate instances by canonicalized directory, so a diamond dependency
  (e.g. two sibling submodules both depending on `../openemail`) deploys once.
- Detect dependency cycles.
- Compute per-project `PUBLIC_CANISTER_ID` bindings on each Canister so a
  dependency's canisters keep their standalone view (own siblings by bare name)
  while the parent sees only the exposed dependency canisters under `<alias>:`.
- Reserve `:` in canister names and dependency aliases; validate alias
  uniqueness and alias/canister collisions.
- Translate dependency canisters' name-based controller refs to store keys.

The canonical `Canister` gains a `bindings` map consumed by the deploy step
(next commit). For a project with no dependencies, bindings map every canister
to itself, preserving today's flat env-var behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…store namespaced-name safe

- set_binding_env_vars_many now derives each canister's PUBLIC_CANISTER_ID env
  vars from its own `bindings` map instead of a single flat set, so a dependency's
  canisters keep the view their project expects and the parent sees only the
  exposed dependency canisters. Bindings are filtered to ids present in the
  environment; a dependency-free project is unaffected (every canister still sees
  every sibling).
- The artifact store percent-encodes `/`, `\`, `:`, `%` in canister names so
  namespaced dependency canister keys (e.g. `vendor/openemail:backend`) are valid
  filenames on all platforms. Plain names are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A dependency canister's namespaced name (e.g. `dep:backend`, `vendor/dep:backend`)
is not a valid DNS label, so it cannot get a friendly `<name>.<env>.<domain>`
subdomain. Skip the friendly host for such names and use the principal host,
avoiding a malformed/incorrect URL when printing deployed canister URLs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add examples/icp-project-dependency demonstrating an app that vendors an
  `openemail` dependency and exposes its `backend` canister.
- Add an integration test that deploys an app + dependency to a managed network
  and asserts the app sees PUBLIC_CANISTER_ID:openemail:backend while the
  dependency's canisters keep their standalone view.
- Document dependency projects in the canister-discovery guide and CHANGELOG.
- Move the store_artifact test module to the end of the file (clippy).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ndencies

Cover the layout where two independent sub-projects (service-a, service-b) each
depend on the same sibling `openemail` via `../openemail`, and an app depends on
both services.

- Add examples/icp-project-dependency-shared demonstrating the umbrella layout.
- Add an integration test that deploys the app + both services + openemail to a
  managed network and asserts openemail is deployed once and both services'
  `PUBLIC_CANISTER_ID:openemail:backend` resolve to the same shared canister id.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant