Ask HN: Why does every package+module system become a Rube Goldberg machine?
Lots of brain cycles are spent on "programming language theory". We've roughly figured out the primitives required to express real-world computation.
In contrast, we apparently have no "package management theory". We have not figured out the primitives required to express dependencies. As a result, we keep building new variants and features, until we end up with <script>, require(), import, npm, yarn, pnpm, (py)?(v|virtual|pip)?env, (ana)?conda, easy_install, eggs and wheels ...
Is it just a "law of software" that this must happen to any successful language? Or are there examples of where this it has not happened, and what can we learn from them? Is there a "theory of package management", or a "lambda calculus of package management" out there?
221 comments
[ 2.9 ms ] story [ 226 ms ] thread- Cross-platform - Wraps another packaging format - Lowest disk space - Dependency version solving - An OS can run make changes to its own low-level state using its own tools - User can configure when and what are installed
Even Nix, which has its own language and isolation model, can’t meet all these.
It's cross-platform to some degree. If Windows is a requirement for this, I see why it doesn't qualify.
Nix wraps other packaging formats all the time. There's endless x2nix converters.
In a way, Nix is "lowest disk space" - by nature of lazy evaluation, Nix closures are always exactly what they need. Now, they may "actually" need less and are holding onto more due to how packages are written by people. And you do need to GC.
Dependency version solving can actually be punted to other tools. In Haskell, Nix uses a combination of cabal and pkg-config to handle Haskell + C deps.
NixOS can swap out the kernel along with anything else with a line or two in your config.
Back when those languages were designed, you'd manually download the few modules you need, if you downloaded any packages at all. In C you'd normally build your own world, since it came before the www times, and C++ kind of inherited that. But languages which came out later decided that we now live in a world where most of the code that is executed is packages, most likely packages which live on Github. So Julia and Rust build this into the language. Julia in particular with the Project.toml and Manifest.jl for fully reproducing environments, its package manager simply uses git and lets you grab the full repository with `]dev packagename`, its package registry system lets you extend with private package worlds.
I think the issue is that dependencies are central, so you can never remove old package systems because if that's where the old (and rarely updated) dependencies live, then you need to keep it around. But for dependencies to work well, you need all dependencies to be resolved using the same package system. So package systems don't tend to move very fast in any language, whatever you had early has too much momentum.
I cringe hard when I see projects depending on git repos, without pinning a version or commit.
The package manager requires a canonical URL for the git repo. The location of dependencies doesn't matter, for example Github repos with Bitbucket dependencies work fine. The FastLapack packages lived on BitBucket at one point, and then migrated to Github, and that happened without a hitch to upstream packages like LinearSolve.jl or DifferentialEquations.jl.
Also, it takes a one line change to General change a package's from Github to BitBucket. It didn't take me more than 2 minutes when I tried. Did you not have a similar experience? Can you show the PR where you gave it a try? Or is your response just speculation?
The package server mirrors the repos, so even if these services went down it would not break the dependency chain (developers wouldn't be able to easily update the packages, but the installations and all would still work). So if tomorrow Github decided to make all of the code private for some odd reason, Julia's package server would continue to serve the current versions.
I really should have looked into it more before commenting, especially since I knew Cargo is actually a well-designed and considered package system.
Is it really a proxy in this sense? I was under the impression that it did not keep cached copies of the code, just some metadata, so that it didn't actually protect about someone removing their repo. Maybe I'm wrong about that though.
My impression, particularly based on the timing, was that the introduction of the proxy was in part directly in response to the "left-pad" fiasco. One thing it will also do is automatically publish your docs, if it can detect an open-source license. That would certainly require keeping a copy around.
Following from this, I think that most back-end applications should try solve their messy runtime environment issues with some containerization. Java/C(++) however...
I'm running macOS with Macports, and oh boy. For a long time, the aws and eb CLI tools had different package requirements or whatnot, and you couldn't install both of them simultaneously for whatever reason. Some stuff installs fine with the system Python, some stuff needs Macports for a specific Python version, some stuff needs root access to install itself... anything Python is a hot mess.
First, one never touches the system Python. It's there for OS-managed stuff and to run OS components.
Then, every Python-based tool should be installed in a separate, unprivileged virtualenv, with executables symlinked where you find them appropriate (usually ~/bin or /usr/local/bin).
If you find yourself ever issuing a `sudo pip install`, chances are million to one that you are doing a disservice to to yourself.
Why would one want to not use what the system provides? Everything not provided by the OS is additional maintenance burden on myself. The 'nix world has managed just fine with OS-provided bash, perl and other runtime dependencies for decades.
> Then, every Python-based tool should be installed in a separate, unprivileged virtualenv, with executables symlinked where you find them appropriate (usually ~/bin or /usr/local/bin).
No one has time for all that. I'm not a Python developer - as a user I'm happy enough if it barely works.
When an ecosystem requires messing around with completely separate instances of the runtime for each program, that's not a good sign for the quality of the ecosystem as a whole.
Pip has lots of issues (IMO it's one of the worst package managers):
- It installs dependencies globally by default! This makes it a massive pain to work with unless you get virtualenvs setup correctly. Which you have to remember to do every time you interact with a project.
- It doesn't use lock files by default. You have to remember to lock your dependencies.
- Packages that depend on C libraries have a tendency to fail with cryptic error messages if you don't have the library installed (which can be contrasted with node.js packages which tend to bundle the library, and will even compile it from source for you if there isn't a binary version available for your platform).
1. Clear definitions of what is an artifact that it manages, easily described in a versioned file but still separate from your source code
2. Simple support to combine internal and external sources
3. Support for interacting with other package ecosystems (you can easily write a pom.xml for any other kind of package and reference it from your Java projects)
4. Not picky about version number formats
5. Not reliant on other tools to handle any step of package management (except for compilation, of course)
6. Works at the project level and has no problems about different versions being used in different projects on the same machine (with a shared cache for efficiency)
A lot of posters have said that Rust and Go work well.
Honestly, the OP sounds like the stereotype of a JS developer: "This is a problem for Javascript, therefore it must be a problem for everyone else, too."
No, guys, it's just you. Every single time.
You could argue many of these are not Python's fault, some can probably be attributed to carelessness on third-party and tooling devs or on my part. But let's invent a metric here: "% of time spent dependency troubleshooting over total programming time". I've had better experiences with this in other languages, and intuition tells me that my carelessness or that of third-party devs being equal, there is something not quite right about the Python dependency management story. I put up with it because like many others I think that the language is otherwise dope.
and boy would you be wrong. I just tried to install `datasette-scraper` on my system. This didn't work because it would consistently pick up outdated versions of Django and `more-itertools` from the system's Python site-packages directory.
By the way I learned to (1) install an updated version of Python that has the advantage of not being the one that the system uses; (2) do not use `pip` the executable but `python3.9 -m pip`; (3) use `pip` with argument `--target vendor` to install everything in a dedicated project-owned directory; (4) prefix my invocation of the software with `PYTHONPATH=./vendor` to add that to the import path; (5) add a `vendor/sitecustomize.py` file with the lines `import sys; sys.path.pop()` to remove the system's `site-packages` from the path.
That's a local installation of a Python package in five easy steps, no 'virtual-env' or some such! Pip is great! /s
I'm pretty sure you're manually doing the same thing virtualenv (venv in python3) does for you...
Also, PIP can't evolve, because nobody will make their packages even more complex to support PIP only idiosyncrasies. It doesn't matter that everybody uses PIP, you have to spend 90% of your time dealing with other package managers, so they are the ones on your mind all the time.
My experience is that the older gen languages you mention had to invent package management, made lots of understandable mistakes and now are in a backwards compat hellscape.
Rust and Go built their packaging story with the benefit of lessons learned from those other systems, and in my experience the difference is night and day.
That was my impression some time ago.
But last week I attempted to compile a couple of (not very big) tools from cargo. And it ended up downloading hundreds of dependencies and gigabytes of packages.
Looks like node_modules.jpg all over again :(
I wrote a post highlighting Go's mod system: https://verdverm.com/go-mods/
imo, it is the best designed dependency system I know of. One of the nice things is that Go uses a shared module cache so there is only one copy on your computer when multiple projects use the same dependency@version
The best solution always lies somewhere in the middle. But finding this “middle” (and adhering to this approach) is the hard part.
I don't see it being talked about here, either. Our current system of pulling in packages made by random people on the internet is going to burn us. We assume that everyone who creates a package is an honest, reliable, developer who will not inject malicious code into their package. This assumption is similar to the assumptions we made around SMTP, HTTP, DNS, and every other internet protocol. Turns out we were wrong and surprise! you can't trust people on the internet.
I'm not sure if we can solve this with package managers. But package managers are part of the culture that has created this problem, and are probably a reasonable starting point to try and address it.
[0]:https://david-gilbertson.medium.com/im-harvesting-credit-car...
The rest of the stuff around modules, like crippled constraints, zero control over contents (which they have changed!), and completely non-existent "x is available, upgrade" or release tooling: constant, unnecessary pain, and it'll be inflicting serious damage on the ecosystem for many years to come.
Do you mean ranges on dep versions? The way it is currently, the version you set is the minimum, and the algo finds the highest minimum set across all deps and uses that.
If ranges were introduced, you'd end up with an NP hard problem and need a SAT solver for your deps again
> Release tooling
What are you looking for here? Libraries only need to push a git tag, binaries do require a bit of work, but Goreleaser fills that pretty nicely. It would seem hard to standard where binaries would be pushed
> completely non-existent "x is available, upgrade"
https://go.dev/doc/modules/managing-dependencies#discovering...
"go list -m -u all"
Go's algorithm is much simpler and does not need a lockfile while still giving deterministic results. Ranged deps without a lockfile cannot. There is benefit to two people running the same command and getting the same dependency versions. Most projects do not start with a lockfile, so it is quite easy to have different versions when running getting started commands.
Another example, if I install two ranged dependencies in both orders, will I get the same final deps@version list?
Why choose SAT over simpler deterministic algos?
Further, another major difference is that you don't need those dependencies after you've built. You can blow them away. Doing that with node is not as straightforward, and in many cases, not possible.
Plus none of them handle binary library distribution as some of the packing models that came before them.
[1] https://go.dev/blog/migrating-to-go-modules current system
[2] https://news.ycombinator.com/item?id=34310674 goproxy agressively polling sourcehut
It's worth recognizing that go isn't alone in dns-based namespacing : java's maven/gradle use the same strategy: https://repo1.maven.org/maven2/gov/nih/imagej/imagej/1.47/
We should have left Commonjs a long time ago, while keeping backwards compatibility.
At the same time what I see with the node+npm system is that everything is just ,,it just doesn't work by default''.
Having 10 other package managers doesn't work either, they are faster, but don't solve this problem.
This one issue (IMO) is preventing parts of the ecosystem from switching to esm.
I can go on, but it's a terrible hodge-podge of systems. It works nicely for simple cases (consuming libraries off Github), but it's awful when you go into details. And it's not even used by its creators - since Google has a monorepo and they actually use their internal universal build tool to just compile everything from source.
The flip side of this is that it never has to worry about naming collisions or namespacing: Your public package name must be a URL you control.
Additionally, there is no requirement for a centralized package facility to be run. The Golang project is currently running pkg.go.dev, but that's only been in the last few years; and if they decided to get rid of it, it wouldn't significantly impact the development environment.
Finally, the current system makes "typo-squatting attacks" harder to do. Consider the popular golang package github.com/mattn/go-sqlite3. The only way to "typosquat" the package is to typosquat somewhere up the dependency tree; e.g., by creating github.com/matn/go-sqlite3 or something. You can't typosquat github.com/mattn/sqlite3, or github.com/mattn/go-sqlite, because you don't own those namespaces; whereas with non-DNS-based package systems, the package would be called `go-sqlite3`, and `sqlite3` or `go-sqlite` would be much easier to typosquat.
All those things I find really valuable; and honestly it's something I wish the Rust ecosystem had picked up.
> It requires users of your code to update all of their import statements throughout their code whenever you move your hosting.
This is a necessary cost of the item above. It can be somewhat annoying, but I believe this can be done with a one-line change to the go.mod. I'd much rather occasionally deal with this.
> It requires you to physically move all code to a new folder in your version control if you want to increase the major version number.
And the benefit of this is that legacy code will continue to compile into the future. I do tend to find this annoying, but it was explicit trade-off that was decided back when they were developing their packaging system.
Packaging is a hard problem, with lots of trade-offs; I think Go has done a pretty good job.
One way in which Go and Rust have it easier than Python or Node is that the former only have to deal with developers; the latter have to deal with both developers and users, whose requirements are often at odds with one another.
Non-DNS-based packages don't have to be named "go-sqlite". You can easily require some namespacing, and even use DNS as a base for that, but having an abstraction over it that recognizes the specific needs of package management is better. For example, Maven packages are called things like org.apache.commons, and registering a new package requires control of an equivalent DNS domain. However, if you later lose control of that domain, the new owners don't simply get to replace the packet in Maven just because they snipes your domain.
Go's choice to require full paths for import in each file is also not a direct implication of the previous item - they could have allowed the go.mod file to specify the path and a name, and then allow source files to import based on that name. Instead, this comes from Go tooling that existed before modules support, when the tooling would scour all files in your project to find dependencies.
Moving code to a v2 dir does not specifically help (or hinder) with backwards compatibility. Old code can always simply keep using the old versions of the package anyway. It is also a very unpopular decision, with very few packages actually adopting v2 versions precisely because of this requirement, even when making major breaking changes. Even the team maintaing the Go protobuf bindings decided not to use v2 when they rehauled their code (opting instead to create a new v1 at a new location with minor versions starting at 1.20...).
Sure, packaging is hard, but the Go team has chosen to go against the flow of essentially all other package managers, and instead of learning from their mistakes, they seem to have decided to make original mistakes all their own.
This removes a whole slew of attack surfaces that JS and Python have, and leverages an existing “prove you are who you say you are” infra
The advantage being, if you later lose access to the DNS domain that you used to publish an artifact to Maven Central, the new owner doesn't automatically get to compromise your artifact for all (new) users.
> It requires you to physically move all code to a new folder in your version control if you want to increase the major version number.
This is untrue.
> It requires users of your code to update all of their import statements throughout their code whenever you move your hosting.
This is only true if not using a vanity URL, but is sadly often the case.
> It takes arcane magic to support multiple Go modules in the same repo.
I don’t know what you’re calling arcane magic here, but we maintain repos at work with 6-7 go modules in without it being an issue whatsoever, and no “arcane magic” required, so I’m going to go ahead and say this is untrue too.
[1] https://stackoverflow.com/questions/10687627/relative-import... [2] https://stackoverflow.com/questions/57806081/how-to-access-n...
I was indeed wrong, this is not necessary, though it is the original strong recommendation of how to do it from the original modules proposal.
Still, slightly changing the critique to say that changing major version is a big hassle, and it requires touching all of your project files, and pointing out how bizarre the official recommendation is compared to other packaging systems keeps my point intact, I believe.
> This is only true if not using a vanity URL, but is sadly often the case.
Yes, I am aware that you can buy a custom DNS and point it to your repo to release under a better name, but it is almost never done (I think the only dependency the project I work on has that does this are Google's Go protobuf bindings and several k8s.io projects).
> I don’t know what you’re calling arcane magic here, but we maintain repos at work with 6-7 go modules in without it being an issue whatsoever, and no “arcane magic” required, so I’m going to go ahead and say this is untrue too.
If you want to maintain multiple Go modules in the same git repo, and you want others to be able to download them, you need to tag commits for each module you are releasing. These tags must be formatted to match the dir path to the specific module's go.mod file (which also "conveniently" becomes part of the module name), except for the afore-mentionet vX directories if you chose to follow the Go team's recommendations for major versions. Then, for local development, you also need each go.mod to contain a REPLACE directive for each other module inside the same repo (or maybe directives in a go.work file, or who knows what else). Overall, this ends up creating lots and lots of useless tags, and is the definition of what I'd call arcane.
You'll not find the tag format in any "getting started" doc, and you'll not get any help from go mod itself if you get any of this wrong - just some "not found" style errors.
Edit: oh, and I should note: I have no idea how this is supposed to be done if your repo is not in Git, even though go modules are supposed to support other VCSs too.
That's simply not true. That's only one way you can do it. Another way is to create a branch.
https://sourcehut.org/blog/2023-01-09-gomodulemirror/
Version incrementing, packaging wasms, dancing around code generation – all doable, but not standardized.
There's a release-please to automate all that, but it's not an easy task to set it up in all of your repos.
Besides, if in addition to Rust projects, you have projects in other languages like JavaScript, then you have to do it twice and struggle with understanding all of the package management systems provided by all languages you have.
A single swiss-army-knife package manager would be amazing.
And given that packages have to depend on other packages, and cannot depend on a git repository, that feature is mostly useful for testing bug fixes, private repos for leaf packages, stuff like that.
That the entire code of all dependencies is hosted in one place is an important differentiator of crates.io vs what Go does, which is why this is relevant in this context. Crates.io is centralized, with all of the advantages and disadvantages that that brings, and Go is decentralized, with all of the advantages and disadvantages that brings.
I feel like you've picked the 3 worst ecosystems as an example.
Java has a wonderful ecosystem. You can use Maven/Gradle/Bazel as a dependency manager/build tool. All support the Maven repository format. Packages are published as jars and easily consumed. Of course you can still end up in dependency hell and you should take steps to mitigate those issues, but I'm quite happy with it.
> As a result, we keep building new variants and features, until we end up with <script>, require(), import, npm, yarn, pnpm, (py)?(v|virtual|pip)?env, (ana)?conda, easy_install, eggs and wheels ...
IMO the problem here is that there isn't a monopoly and its easy to swap in a replacement. JavaScript/Python developers are happy (or willing) to try something new, so new alternatives are created. Convincing a Java developer to try a new build system is difficult, so changes are made to existing systems rather than creating new ones.
Go and Rust are two other systems that work well and are (almost?) universally adopted. They come with the language and there is little reason to look around.
FWIW as far as JavaScript on NodeJS goes, npm did set the standard quite firmly. You can also use yarn or pnpm, which promise improvements over npm while using the same package metadata (`package.json`) and produce npm-compatible structures in the same subdirectory (`node_modules`), so overall the user experience is muche like going from `sudo apt-get install` to the newer, more polished and otherwise largely equivalent `sudo apt install`.
It's no coincidence that Rust (which has basically copied Haskell's type system to a large degree) and Julia (which is essentially a Lisp in disguise) have somewhat sound packaging systems. C/C++ does not even have a tidy core language, let alone proper modules (yes, yes, C++ modules might change that, but they are still a complex mix of templates and normal code).
The public interface of a module has to be explicit enough that a compiler for that language only needs the public interface description of that module to compile code that depends on it.
A good example of the most widely used and successful module system is C compiled libraries. The header files represent the public description of a library (or, for dynamic linking, the .so/.dll itself embeds this info as well), and a C compiler only requires these header files to be able to compile dependent code and later link the two. It is also impossible to access non-public members of the library from standard C (without resorting to direct memory access, of course).
Java packages don't have these properties because of two problems: there is no way to indicate dependencies between packages, and there is no standard way to have a bundle that corresponds to a package (since different .jar files can all load classes into the same package). The Java module system was created specifically to address these problems (and is still not fully adopted throughout the ecosystem).
C++ breaks C's module system because of its heavy reliance on templates, which have to be entirely declared in the public API and can't have any internal API of their own; and because the compiler needs to know the total size of a class, including the size of its private members, to compile any code which references objects of that class.
For Python, I'm honestly not sure if Python packages fit this definition of a module or not.
Other languages with generics and modules do just fine by having the required metadata on the module, e.g. Eiffel, Modula-3, Ada, OCaml,..., which is what C++20 modules do as well.
I have been using C++ modules on VC++ for quite some time, and while there are still some warts on the overall experience, they work quite alright.
Also, C# Assemblies solved this problem for generics too, for a more mainstream example.
It's a form of not-invented-here syndrome and you can see it in all the comments defending their favorite language..
> we apparently have no "package management theory".
Apparently. In reality there's been plenty of research - and for decades - that is not being used.
Can you link what you're referring to? Genuine request, I'm curious and keen to learn more.
Dotnet has nuget and it is quite pleasant to use in my experience. I think the big deal is having some standardized way of managing them as a layer on top. In teh dotnet case, it is having the solution pull in the nuget packages, rather than individual files. Along with having a standardized way to index available packages from each source (In this case the nuget json)
I do agree it is an issue. Doing away with node_modules completely on the last rails 7 app I worked on was the most cathartic experience of my life.
There's no reason why a package-management system needs to be language-specific; dependencies are often cross-language. Hell, even some blocks of code contain more than one language.
The package-management system is responsible for deploying packages. The way a package is deployed should depend on the operating environment, not on the language. These language-specific packaging arrangements typically deploy into some private part of the file-system, organized in its own idiosyncratic way.
Using git as a repository is just nuts. Git is privately-owned, and stuffed with all kinds of unaudited junk. You can't audit everything you install by hand; so these systems force you to install unaudited code, or simply not install.
I've been using Debian derivatives for years. I appreciate having an audited repository, and an installation system that deploys code to more-or-less predictable filesystem locations.
This sounds a lot like a case of https://xkcd.com/927/ . Languages have different ways of importing and installing dependencies, trying to create a package manager over all of those is just going to end up making things even more complex, especially if you target all platforms at once.
> Using git as a repository is just nuts. Git is privately-owned, and stuffed with all kinds of unaudited junk.
Git is fully open source. Are you confusing Git and GitHub?
They're not actually different. They call things differently, and they have different methods of passing the required lookup paths/artifacts/sources to their compilers/interpreters/linkers, but in the end all of them are conceptually the same thing.
The concept of a programming language isn't what people use, but the concrete reality of specific implementations on specific hardware.
You could also say that a coffee cup and a donut aren't actually different because they're topologically the same... but the facts on the ground do actually matter.
Do you mean github? Git is open source and one of the few pieces of software that works in a truly distributed fashion.
Also: what do I care that people store unaudited and insecure stuff in there?
Git is owned by the same owner as Linux. If you've been using Debian derivatives for years it seems you must have some trust to give that private entity? Unless the derivative you speak of is Debian GNU/k*BSD?
Furthermore, if you give trust to Debian derivative projects, why not trust their Git builds? If you trust everything else in their distribution Git is a curious omission. Do you have a personal beef with Torvalds or something?
Anaconda is probably the closest as it also package many non python packages. Nix is also similar but it doesn't support Windows at all (without WSL)
1) "npm install" or "go get" or what have you, works on every platform (barring bugs), while "apt install" only works on some.
2) Most platform package managers aren't good at handling multiple versions of dependencies (which, neither are many language package managers, but they're easier to sandbox away with supplemental tools than system package managers are)
3) Most platform package managers lag way behind language-specific package managers, and may also lack tons and tons of packages that are available on those.
> Git is privately-owned
Git... hub, you mean?
Honestly, I'd like to have a lowest common denominator of "just dump the contents of the tar/zip archive" here.
Only check required would be to ensure you're not overwriting anything.
Dependencies and PATH management etc could be done out-of-band. That way you would not have to re-download stuff at least.
https://masysma.net/11/maartifact.xhtml
Downloads tgz if not already downloaded, saves it to directory `../x-artifacts` and optionally extracts it.
Sometimes languages are relased with just a spec and don't want to force any choice of tool or way of doing things on you, so you just manage that yourself or create third party tools, and that's where it gets wild in every direction, but it also creates room for new ideas, innovation, which later are used in "official" modern package managers built in the language tools.
And yeah, nowadays I use Rust even for scripting stuff that would be easier to do in Python, just because I don't want to create a thousandth virtualenv, find a lib that does what I want but it's for Python<3.6 ou Python2 etc., so in the end it's easier in Rust even though some new libs require nightly builds of the toolchain.
we use the urfave/cli library, which works great, and gets you a ton of things for almost free (passing and documenting flags and arguments) that you just don't get easily in bash
As I've said, I still love it and use it a lot, easy to write, easy to explain and share, easy to run without dependencies and if your code runs the same on different versions of Python, but hard to choose which type of virtualenv you want to use, then pick the right versions of dependencies that work together (if you have big dependencies like in data science for example, and some of them are less maintained than others, which happens because the Python ecosystem is rich but people start projects and move on, happens everywhere but because it's easy to start a project in Python, it's also easy to leave it unmaintained).
I love explaining Python code to non coders!
We have a good hunch. The basic theory behind Nix definitely goes in the right direction, and if we look away from all the surface-level nonsense going on in Nix, it's conceptually capable (e.g. [0]) of being a first-class language dependency manager.
For this to work at scale we'd need to overcome a couple of large problems though (in ascending order of complexity):
1. A better technical implementation of the model (working on it [1]).
2. A mindset shift to make people understand that "binary distribution" is not a goal, but a side-effect of a reasonable software addressing and caching model. Without this conceptual connection, everything is 10x harder (which is why e.g. Debian packaging is completely incomprehensible - their fundamental model is wrong).
3. A mindset shift to make people understand that their pet programming language is not actually a special snowflake. No matter what the size of your compilation units is, whether you call modules "modules", "classes" or "gorboodles", whether you allow odd features like mutually-recursive dependencies and build-time arbitrary code execution etc.: Your language fits into the same model as every other language. You don't have to NIH a package manager.
This last one is basically impossible at the current stage. Maybe somewhere down the line, if we manage to establish such a model successfully in a handful of languages and people see for themselves, but for now we have to just hold out.
[0]: https://code.tvl.fyi/about/nix/buildGo
[1]: https://cs.tvl.fyi/depot/-/tree/tvix/
I'm in a team that works on a pet prog lang for distributed systems, and we did some research of using an existing package managing systems. We've settled on NPM for now, but god I wish there would be a better generic package manager out there.
Again, maybe you're already aware of it, but I think it's a nice example of genericising a concern common to many languages which sounds similar to what you're asking for (albeit unfortunately in a slightly different space).
[0] https://github.com/asdf-vm/asdf
https://asdf-vm.com/manage/configuration.html#tool-versions
Similar to an `.editorconfig` to globally define stuff like tab widths for IDEs.
https://docs.racket-lang.org/denxi-guide/index.html
[0] Edit: rebuilding your dependencies if you need it and handling that seemlesly CAN be hard on Debian. Something that even Nix struggles with even if they are best in class by far. It is also completely different from the notion of compiling a program. I wonder what you consider the goal of a package system.
Quite likely! The whole concept of separately building a source/binary package, and then uploading/"deploying" that binary package, already violates the notion of being "just a cache" for me. There might be a tool in Debian where I can seamlessly say "do not download this package from the repository, but build it locally" - but even if so, it would surprise me if it can give me the same guarantees as Nix (i.e. guarantees about the artifact being repeatable, and being addressable by its inputs rather than a human-curated tag or version number).
> Something that even Nix struggles with
Nix only struggles with it to the degree that some of the higher-level abstractions in the existing package set (which are built on top of the fundamental model, not part of it) can be confusing/underdocumented, but conceptually this is a simple thing to do in Nix. Even practically at this point it is usually simple - unless you're dealing with Haskell or Javascript, of course.
> I wonder what you consider the goal of a package system
I want it to let me describe a desired state, and then make it so. That state is best represented as a graph, like a Merkle tree, of the instructions for the individual steps and a way to address them. An individual step is a transformation (i.e. some program) executed over some sources, yielding some result (likely in the filesystem). I want any distribution of binaries to be a result of using this addressing scheme, and looking up/copying an already built equivalent artifact for that thing. I want a text file containing the string "Hello HN" to be represented by the same abstraction that represents a specific .so file, the `emacs` package, the configuration of my entire system, the configuration of a cluster of systems and so on. I want this system to be programmable so I can work around the shortcomings that its original designers missed.
Nix (and Guix) do large parts of this already, and are conceptually (though not yet technically) capable of doing all of it. Technically, so is something like Bazel - but its complexity and maintenance requirements make that prohibitively expensive (basically only Google can use Bazel like that, and even they have delineated areas where they "give up" on wrapping things in Bazel).
apt-get --build source package_name
It's unlikely to be fully repeatable to the point of producing an identical binary, but it's also not some esoteric set of procedures.
It's been there for 15 or 20 years. Also Debian has been doing reproducible builds before Nix.
In order to reproduce a given Debian package from scratch, one would need to track down long chains of historical versions of packages from sid. Due to circular dependencies, these chains probably snake back to the very first versions of Debian before the availability of snapshot.debian.org. It’s possible that human judgment could be used to break some chains at an earlier point by substituting different versions, but this goes outside the guarantees provided by Debian’s reproducibility.
Nix guarantees that every package is built with the specified versions of its dependencies, and so on recursively. Some more rebuilding is necessary to maintain this guarantee, but in exchange, it enables stronger notions of reproducibility that Debian can’t.
(The work at https://www.gnu.org/software/mes/ aims to minimize the needed binary seed to a human-auditable size, and should be applicable to Nix and Guix.)
Has it? What I remember being the standard practice for customizing packages was
fewer than 15 years ago. And it's not much like what the parent poster has in mind. It modifies the global state of your system, it requires multiple steps, it produces a different artifact than you might get in the repos at any given time, it leaves things in your CWD on your filesystem owned by your current user, whatever.(Some of that may also apply to `apt-get source --build`, idk.)
With Nix or Guix, when a cached binary artifact for a given package is missing, you may not even notice, the fallback to building from source is so transparent.
I encourage you to actually try Nix or Guix, including writing packages for them, and including adding a patch or overriding a build flags in an existing package, to see how the model is really different.
> Also Debian has been doing reproducible builds before Nix.
What Debian does for reproducible builds and how Nix embodies reproducibility are qualitatively different. If you understood both, you'd understand why this is a silly point to raise.
(Debian's work on reproducible builds is great. Every distro benefits from most of it, and everyone in the Nix community is grateful for it.)
I consider this to be one of the main drawbacks of these systems -- it assumes that every machine is build-capable. I do not want compilers installed on my production servers (nor would I want my RPi to attempt to rebuild half the world because the package server was down). I'm not sure how Nix handles this, but I'm pretty sure that's not possible with Guix.
It doesn't, fortunately. You can tell Nix to rely exclusively on the binary cache, or configure it to have 0 build threads locally and use remote builders.
For a server deployment, many popular choices don't have the server build anything, and in some cases it may not even have any users who are allowed to invoke `nix-*` or anything.
> you'd understand why this is a silly point to raise
It isn't.
(This is a huge issue in our field in all sorts of ways, and a constant source of both pain and compromise)
Things work in NixOS without the need of any of that. So, no it is not necessary.
It's a need created purely from the other directives of the Debian organization.
The difference between almost all Linux or BSD packaging systems and nixpkgs is like the difference between dynamic and compile-time-type-checked programming languages.
It doesn't require the same kind of patching as Nix does (although sometimes patching does help avoid requiring those scripts).
Check it out: https://distr1.org/
Everything else is mostly written to teach people how to use Nix, and the more recent the thing is the more it will focus on surface-level features of the C++ implementation of Nix.
[0]: https://edolstra.github.io/pubs/phd-thesis.pdf
https://www.channable.com/tech/nix-is-the-ultimate-devops-to...
(It doesn't go very much in depth on the conceptual model, but touches on the the main ideas)
And sure, may be you're right that distro packaging is the "wrong model," again, that is the problem then for distros, users are stuck using ubuntu or whatever so they don't have the option to do the "right" thing, so they do use the mishmash of packaging/repo systems as just the cost of doing business.
For development you may want to test your code against multiple versions of the system libraries. This is not easy using a distro package manager.
I do think a Nix-like tool to be used for writing/replacing package managers for new programming languages would benefit from some dependency resolving functionality, instead of just saying 'shove it in a monorepo or pin via VCS refs instead of semver'. Is that not necessary?
Nixpkgs doesn't go down that road because it would be unwieldy at their scale, but more specialized nix-based tools do.
Nixpkgs doesn't do version or feature resolution, but other tools (that are not nixpkgs) can and do.
The next step is then realizing you actually don't need the useless legacy version numbering scheme at all, and that you wasted that effort for nothing.
I'm not sure what you mean, through, by not needing the actual program versions.
Version numbers are an attempt at content-addressing from the time when software came on floppy disks and CD-ROMs. It sorta made sense given the constraints of that technology.
Nowadays trusting git tags over git revision hashes makes no real sense. (Except that git tags are kinda shorter to type, but who cares.)
1. The package definitions are just a normal, battle-proven, very well defined general-purpose, functional-style supporting programming language (Scheme).
2. There is no conceptual difference between a package definition in the public Guix system, and a self-written package definition which a developers makes to build and test his own package, or to build and run a specific piece of software. The difference is equally small as between using an Emacs package, and configuring that package in ones .emacs configuration file.
I think the world is complicated and you will in the end need to support compilations/installations on AmigaOS 2.05 on m68k running in kubernetes native.
But to fully appreciate it, it helps to understand syntax transformation in Racket. Once the rigorous phase system forces non-kludgy static rules about when things are evaluated, your syntax transformers and the code on which they depend could cause a mess of shuffling code among multiple files to solve dependency problems... until you use submodules with the small set of visibility rules, and then suddenly your whole tricky package once again fits in a single file cleanly.
I leveraged this for some of my embedded doc and test experiments, without modifying the Racket core. (I really, really like single-source-file modules that embed doc, test, and package metadata all in the same file, in logical places.)
The proxy thing and the drama with sr.ht is annoying, but maybe it was solved while I didn't look.
Or was that open source software? Or was that any private corporate software ecosystem? If I squint the problems all start to look the same. I must need glasses, considering my squint frequency.
No 3rd party Rube Goldberg contraption needed. No package managers or dependency resolvers, no special build tooling, no lock files. No constantly having to run updates and figure out what they broke by way of transitive dependencies. Just get what you need and use it.
I haven't, however, developed enough professionally with it to know how that plays out long-term in practice. But I'd have to imagine it would compare favorably to just about everything else nowadays. The things I've been using for the last couple of decades are all pretty abysmal in comparison.
I'd say they were a solution in search of a problem, but people have managed to create a problem to justify the solution, which really still isn't very good. At least, not nearly as good as the simple get, add, use pattern.
Step 1: look at all the existing package+module systems, and realise they are all horrible Rube Goldberg machines, way too bloated for the simple thing you want to do
Step 2: write your own package+module system! It's lean, mean and just does the simple obvious thing your simple one-man project needs.
Step 3: your system hits the real world! Your system is used with projects that involve more than one person and grow organically, and that spread to more and more esoteric environments. The Rube Goldberg projects have Rube Goldberg aims and their target ecosystems have Rube Goldberg requirements while goalposts shift continuously with tight deadlines. You add features and toggles to your system to support these things.
Step 4: congratulations! You are the proud inventor and owner of yet another Rube Goldberg machine. It is now time to move on to better things; the system, meanwhile, is handed over to some committee and will only grow in complexity from here.
https://www.kiplingsociety.co.uk/poem/poems_palace.htm