Dependency management #7

Open
opened 2025-12-13 17:29:47 -05:00 by Ghost · 5 comments
Ghost commented 2025-12-13 17:29:47 -05:00 (Migrated from codefloe.com)

There should be two types of dependencies:

  • Runtime dependencies, placed under the dependencies key.
  • Development dependencies, placed under the dev_dependencies key. This includes things like build dependencies and test dependencies.

Running nc add <dep> will add it to dev_dependencies by default, but if the package is used in some way such that any users of the library will require it, it will automatically be moved to dependencies.

# nc.toml

# ... other metadata

[dependencies]
dep = "v1.0.0"
# it will be automatically moved here if necessary

[dev_dependencies]
dev_dep = "v0.2.0"
# `nc add <dep>` will put it here
// nc.lock

{
  // ... other metadata
  "dependencies": ["dep@v1.0.0"], // array of dependencies
  "dev_dependencies": ["dev_dep@v0.2.0"], // array of dev_dependencies
  "packages": { // map of packages = dependencies + dev_dependencies
    "dep@v1.0.0": ["otherDep@v1.1.0"], // each package lists its dependencies (not dev_dependencies)
    "dev_dep@v0.2.0": [],
    "otherDep@v1.1.0": [], // deps of deps also get listed here
  }
}

This way, users of a library will have the minimum number of dependencies, while also creating minimal hassle for library maintainers, as nc.toml effectively manages itself.

There should be two types of dependencies: - Runtime dependencies, placed under the `dependencies` key. - Development dependencies, placed under the `dev_dependencies` key. This includes things like build dependencies and test dependencies. Running `nc add <dep>` will add it to `dev_dependencies` by default, but if the package is used in some way such that any users of the library will require it, it will automatically be moved to `dependencies`. ```toml # nc.toml # ... other metadata [dependencies] dep = "v1.0.0" # it will be automatically moved here if necessary [dev_dependencies] dev_dep = "v0.2.0" # `nc add <dep>` will put it here ``` ```jsonc // nc.lock { // ... other metadata "dependencies": ["dep@v1.0.0"], // array of dependencies "dev_dependencies": ["dev_dep@v0.2.0"], // array of dev_dependencies "packages": { // map of packages = dependencies + dev_dependencies "dep@v1.0.0": ["otherDep@v1.1.0"], // each package lists its dependencies (not dev_dependencies) "dev_dep@v0.2.0": [], "otherDep@v1.1.0": [], // deps of deps also get listed here } } ``` This way, users of a library will have the minimum number of dependencies, while also creating minimal hassle for library maintainers, as `nc.toml` effectively manages itself.
Ghost commented 2025-12-15 15:20:02 -05:00 (Migrated from codefloe.com)

Would the plan be to have like mod.nclang.org, or go the more golang route and do .git repos being the module root, and access see it to a global module root, so something like github.com/codycody31/nc-nano64 -> nano64/nano64 | nano64/utils, etc?

Would the plan be to have like mod.nclang.org, or go the more golang route and do .git repos being the module root, and access see it to a global module root, so something like `github.com/codycody31/nc-nano64` -> `nano64/nano64` | `nano64/utils`, etc?
Ghost commented 2025-12-15 19:48:22 -05:00 (Migrated from codefloe.com)

This issue is actually out of date on how dependency management will work, I'll write up an updated version later. It'll be based on Git repos, but closer to Rust than Go. The dependency file will look like:

[dependencies.some-dep]
url = "https://codeberg.org/user/some-dep"
commit = "<commit hash>"
path = "path/to/module/root" # optional

I'm thinking the module root's lib.nc file can be the entrypoint to the library. So import "some-dep" as somedep will import codeberg.org/user/some-dep@commit/path/to/module/root/lib.nc.

The command line way of adding dependencies will be:

ncc add https://codeberg.org/user/some-dep # defaults to repo root, latest commit, name="some-dep"
ncc add https://codeberg.org/user/some-dep (--commit|-c) <commit hash>
ncc add https://codeberg.org/user/some-dep (--tag|-t) <git tag> # converted to commit hash
ncc add https://codeberg.org/user/some-dep (--path|-p) path/to/module/root
ncc add https://codeberg.org/user/some-dep (--name|-n) <custom name>

The lockfile can then store all the same data, but also a checksum to validate against:

[dependencies.some-dep]
url = "..."
commit = "..."
path = "path/to/module/root" # "." if root path
checksum = "..."

You, as the consumer of the dependency, get the control over what you name the dependency in your code. So for example, you can have two different packages named "some-pkg" without causing naming conflicts, simply by giving them different names in your nc.toml.

Also there are no pre/post-install scripts, nor are there transitive dependencies. If a package you need has dependencies of its own, it needs to declare that in the project README or somewhere. Hopefully that should generally keep the dependency counts low, and avoid the nonsense that tends to happen in NPM, Cargo, Pip, etc. It'll also let you pick and choose the subdependencies you need depending on the parts of the library you actually use. For instance, if you're using an image processing library, but you only need to convert between JPEG and PNG, then there's no reason for you to import subdependencies for all the other image formats.

There's a few more details to work out, and probably a few things that I'm missing too, but that's where I'm at right now. Also TOML isn't a fixed decision for the file format, maybe KDL or something might be better to have a more XML-like structured representation of the dependency tree, which would allow having multiple versions of dependencies and scoping subdependencies to their parents, etc.

Edit: We might also be able to get away without having a separate lockfile, and just having all the config in one file, if we move the checksum to the main config file.

This issue is actually out of date on how dependency management will work, I'll write up an updated version later. It'll be based on Git repos, but closer to Rust than Go. The dependency file will look like: ```toml [dependencies.some-dep] url = "https://codeberg.org/user/some-dep" commit = "<commit hash>" path = "path/to/module/root" # optional ``` I'm thinking the module root's `lib.nc` file can be the entrypoint to the library. So `import "some-dep" as somedep` will import `codeberg.org/user/some-dep@commit/path/to/module/root/lib.nc`. The command line way of adding dependencies will be: ```sh ncc add https://codeberg.org/user/some-dep # defaults to repo root, latest commit, name="some-dep" ncc add https://codeberg.org/user/some-dep (--commit|-c) <commit hash> ncc add https://codeberg.org/user/some-dep (--tag|-t) <git tag> # converted to commit hash ncc add https://codeberg.org/user/some-dep (--path|-p) path/to/module/root ncc add https://codeberg.org/user/some-dep (--name|-n) <custom name> ``` The lockfile can then store all the same data, but also a checksum to validate against: ```toml [dependencies.some-dep] url = "..." commit = "..." path = "path/to/module/root" # "." if root path checksum = "..." ``` You, as the consumer of the dependency, get the control over what you name the dependency in your code. So for example, you can have two different packages named "some-pkg" without causing naming conflicts, simply by giving them different names in your `nc.toml`. Also there are no pre/post-install scripts, nor are there transitive dependencies. If a package you need has dependencies of its own, it needs to declare that in the project README or somewhere. Hopefully that should generally keep the dependency counts low, and avoid the nonsense that tends to happen in NPM, Cargo, Pip, etc. It'll also let you pick and choose the subdependencies you need depending on the parts of the library you actually use. For instance, if you're using an image processing library, but you only need to convert between JPEG and PNG, then there's no reason for you to import subdependencies for all the other image formats. There's a few more details to work out, and probably a few things that I'm missing too, but that's where I'm at right now. Also TOML isn't a fixed decision for the file format, maybe [KDL](https://kdl.dev) or something might be better to have a more XML-like structured representation of the dependency tree, which would allow having multiple versions of dependencies and scoping subdependencies to their parents, etc. Edit: We might also be able to get away without having a separate lockfile, and just having all the config in one file, if we move the checksum to the main config file.
Ghost commented 2025-12-15 21:33:06 -05:00 (Migrated from codefloe.com)

mmm, seems good except for lib.nc, is this not a magic feature? I'd prefer like import("somedep/somedep") since it aligns better. Also, I think we need the spec defined for modules on how to import and then export, ie: if we do a lib.nc, it needs to import submodules and provide access, properly, maybe like somedep.crypto.entropy(), making it use dots to access? since we need a scope management or something, as making everything sit under somedep.cryptoEntropy() etc, or other routes doesn't make sense.

Furthermore, not taking in deps of a dep makes things really hard, since the idea is that the parent marks it as a dep, while the dep itself cannot, which makes using multiple versions of something a bit of a pain, also how would this affect if we import a dep under a different name, but a dep that uses that has it under a different one?

mmm, seems good except for lib.nc, is this not a magic feature? I'd prefer like `import("somedep/somedep")` since it aligns better. Also, I think we need the spec defined for modules on how to import and then export, ie: if we do a lib.nc, it needs to import submodules and provide access, properly, maybe like `somedep.crypto.entropy()`, making it use dots to access? since we need a scope management or something, as making everything sit under `somedep.cryptoEntropy()` etc, or other routes doesn't make sense. Furthermore, not taking in deps of a dep makes things really hard, since the idea is that the parent marks it as a dep, while the dep itself cannot, which makes using multiple versions of something a bit of a pain, also how would this affect if we import a dep under a different name, but a dep that uses that has it under a different one?
Ghost commented 2025-12-16 05:50:58 -05:00 (Migrated from codefloe.com)

These are all good points. Honestly, I might end up reworking the module system entirely eventually, but I want to look a bit more into how other languages do it, and adapt it from there. The current system is very JavaScript-esque, but I'm actually interested in how Zig does modules, and if we can adapt something from that. I imagine the best system lies somewhere in the middle of everything else.

For now, you can implement the design as written on https://nclang.org/syntax/modules (or just ignore it since you shouldn't need to import dependencies outside the stdlib, especially since none exist), but keep in mind that the syntax and system for importing modules may change.

For names: you're right that it wouldn't work. Maybe the config file should have a name field that anyone who imports that library must use.

As for subdeps of deps, I'm not disallowing that, I'm just adding some friction to choosing libraries with tons of transitive dependencies. You'd still be able to have subdeps of deps, with subdep versions scoped to each dep, you'd just have to declare the tree structure manually:

some-dep url="..." commit="..." checksum="..." {
  dep-a url="..." commit="..." checksum="...";
  dep-b url="..." commit="..." checksum="..." {
      dep-c url="..." commit="..." checksum="...";
  }
}

That's why I wanted to use KDL, it makes declaring both properties and children really straightforward, compared to something more object-oriented like JSON/YAML/TOML. XML would also work for this, since it's also a document language, but KDL looks more minimal.

The idea is that if you have to declare your entire tree of dependencies manually, or at least see it in your main config file, then you become far more aware of what you're pulling in, than if it gets hidden inside a lockfile you never bother to open. That, combined with a large stdlib, should hopefully discourage large numbers of dependencies for most things.

These are all good points. Honestly, I might end up reworking the module system entirely eventually, but I want to look a bit more into how other languages do it, and adapt it from there. The current system is very JavaScript-esque, but I'm actually interested in how Zig does modules, and if we can adapt something from that. I imagine the best system lies somewhere in the middle of everything else. For now, you can implement the design as written on https://nclang.org/syntax/modules (or just ignore it since you shouldn't need to import dependencies outside the stdlib, especially since none exist), but keep in mind that the syntax and system for importing modules may change. For names: you're right that it wouldn't work. Maybe the config file should have a `name` field that anyone who imports that library must use. As for subdeps of deps, I'm not disallowing that, I'm just adding some friction to choosing libraries with tons of transitive dependencies. You'd still be able to have subdeps of deps, with subdep versions scoped to each dep, you'd just have to declare the tree structure manually: ```kdl some-dep url="..." commit="..." checksum="..." { dep-a url="..." commit="..." checksum="..."; dep-b url="..." commit="..." checksum="..." { dep-c url="..." commit="..." checksum="..."; } } ``` That's why I wanted to use KDL, it makes declaring both properties and children really straightforward, compared to something more object-oriented like JSON/YAML/TOML. XML would also work for this, since it's also a document language, but KDL looks more minimal. The idea is that if you have to declare your entire tree of dependencies manually, or at least see it in your main config file, then you become far more aware of what you're pulling in, than if it gets hidden inside a lockfile you never bother to open. That, combined with a large stdlib, should hopefully discourage large numbers of dependencies for most things.
Ghost commented 2026-01-17 15:22:22 -05:00 (Migrated from codefloe.com)
name = "ncc"

[lib]
entrypoint = "lib.nc"

[dependencies.some-dep]
url = "..."
commit = "..."
path = "/path/to/module/root"
checksum = "..."

[dependencies.nano64]
git = "https://git.macco.dev/insidiousfiddler/nc-nano64.git"
commit = "a86773b3e4ca1f32407d43b26a2086c9f049cde8"
checksum = "bb8258bc335f05d0a6fd2af4497edd7b9a32871443f534edad026b3bd936df04"
  • lib.entrypoint defines the default file used when importing just the name of the dep, i.e., import "nano64" is actually lib.nc
  • checksum uses sha3
  • deps of deps are not handled currently, and must be defined as flat structure, if a conflict happens.. oh well
```toml name = "ncc" [lib] entrypoint = "lib.nc" [dependencies.some-dep] url = "..." commit = "..." path = "/path/to/module/root" checksum = "..." [dependencies.nano64] git = "https://git.macco.dev/insidiousfiddler/nc-nano64.git" commit = "a86773b3e4ca1f32407d43b26a2086c9f049cde8" checksum = "bb8258bc335f05d0a6fd2af4497edd7b9a32871443f534edad026b3bd936df04" ``` - lib.entrypoint defines the default file used when importing just the name of the dep, i.e., `import "nano64"` is actually lib.nc - checksum uses sha3 - deps of deps are not handled currently, and must be defined as flat structure, if a conflict happens.. oh well
Sign in to join this conversation.
No labels
stdlib
syntax
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
nclang/design#7
No description provided.