Hey - Software engineers can imagine novel ideas. The problem is that every year, there's more code in the OS, and there's more code in web browsers, and there's more applications which depend on all that stuff. Chrome has 25 million lines. At that size, its really hard to innovate on how the browser works.
And that matters because some solutions require changing the operating environment. For example, I'd love to replace the filesystem with an append-only log, and replace files with CRDT based datasets. That would solve all sorts of problems. But doing so would require changing linux, and changing most existing software. Every year that passes, that gets harder and harder to do.
I don't know what the answer is here. Lots of people "solve this" by building further and further up the stack. Linux binaries not feature rich enough for you? We could fix them, but lets use Docker instead. Oh, you want to make a desktop app? You could use the native APIs for that; but who can be bothered learning the native UI APIs? Its much easier to write web app and ship a copy of Chrome on every platform.
We have to find a better answer than going "up the stack" every few years. But so long as people find it easier to write software than read software, its be hard to imagine the system getting simpler over time.
How would you solve the problem of, "I have a finite amount of storage and my drive is full and I need to delete something to make space for this new thing I want"?
Time Machine (apple's automated backup thing) does something similar, at a simpler level
So for example you can tell it to compact all the parts of the log involving that file you've deleted. Create + Delete compacts to a no-op.
2) the way finite storage is handled is that "deleted" content can be garbage collected, and you will then be able to loop around and write to those garbage collected areas. not fundamentally different than an SSD which many times has such a log file system at the hardware level.
The Structure of Scientific Revolutions by Thomas Kuhn is a view of what I’m talking about but in the world of academic theory instead of technology architecture.
It sounds like you're describing tree-shaking, which is already commonplace, particularly in the JS ecosystem. Or LTO dead-code elimination, in compiled languages - neither of which solve dependency hell.
The way to solve the problem of dependency hell is to version every function, and only call functions based on their versions, and ship every old version of every function. Then the application itself, or a dynamic linker, must find and execute the correct version of the function it needs to call. In this way, dependencies can change at any time, but every line of code will only ever call functions that they were originally built and tested against, because those versions are still knocking around somewhere in the dependency. You can upgrade a dependency 50 times but your application will still just keep calling the old version of the dependency's function, and so the behavior of your application remains the same while the dependency receives upgraded functions. You can upgrade any application at any time and it will just pull in the latest dependency, and continue to call the versions of that dependency that they need.
The basic concept for this already exists in glibc as you can ship version-specific functions and link/call a version-specific function. But literally no one uses it. To get widespread adoption and make the paradigm implicit/automatic, we would need to modify the programming language, the compiler, the way programs are executed, require lots more storage, probably new paradigms of caching and layering and shipping code. Package deps would become metadata in a build process, package management would become "scan all executables for dependent versions & download them all".
Nix OS is an attempt to work around all of this by pretending that the problem is just with the linker or the file tree or PATH or something. But it's simply impossible to resolve dependency hell entirely without versioned functions, due to rare but intractable intra-package dependency conflicts. And it doesn't address data model versions or network service interface version changes.
Containers are an attempt to work around all of this by literally having a different copy of the world for every containerized application. It works well enough, except again the boundary between containers is subject to application interface versions and data model versions changing. (The data model and code are basically the same thing since you need code to do anything with data) The only way to have dependency-hell-free containerized applications is if you version their interfaces/data models and have them call the correct version for other containerized apps [or network services], so again you're back to versions of functions.
More specifically, it seems like you're solving a subset of dependency issues (mainly version conflicts), while making the other issues worse (e.g. keeping sub-dependencies up-to-date, ballooning disk space usage, less easily cachable requests to package repos).
Well that's what dependency hell is, a subset of issues: https://en.wikipedia.org/wiki/Dependency_hell The other issues are of course important but require their own solutions. Keeping dependencies up to date [or, in the case of the above method, recompiling applications to use new function versions with security fixes] requires CI/CD, feature flags, auto-upgrading, avoiding drift, etc. Disk space and caching issues require storage with built-in delta layers/CoW/compression (which can also be applied to dependencies, e.g download the dependent versions of functions rather than keeping all of them on disk at once).
As we get more advanced, the hell will get deeper for sure.
This seems like hell to maintain and transitively speaking, a mountain of code. For this much work, I'd prefer to invest in obscenely detailed unittests that allow the team to retire old function-versions and everybody stay on HEAD.
That said, I can imagine cases where old-versions might be helpful for some period of time, and you could call them via their function name and commit hash... then a dependency detector only keeps old versions as needed, a warning tool detects ancient versions to consider retiring, etc. You'd need editor/debugger support so developers can see and interact with old versions, and I'm not sure how this works with raw text editors - perhaps the dependency detector copies the (transitive) code into a new subdirectory?
I think CI/CD practices are really important to get rid of the old code. Automated build systems should be constantly downloading new versions of deps and running tests so devs can fix bugs quickly and release new versions that depend on new deps. The quicker that cycle happens, the quicker old code can disappear, new functionality can be implemented, bugs can be fixed, etc. Because everything would still use pinned versions of deps, it would still work exactly as it was tested, so you wouldn't be sacrificing reliability for up-to-date-ness. Speed and automation of that process are critical.
If this is your vision, why would you dynamically link? If you static link your code to the library functions it calls, the runtime environment can't be changed out from under you (well --- not without a lot of work), and you'd presumably only build with the version of the library functions you like, so you'd be set there too. If you want to update a dependency, pull it in and rebuild.
I don't think this is a popular vision, because people want to believe that they can update to the latest OpenSSL and fix bugs without breaking things and sometimes, they can.
You still have a difficult problem when you share data with code you didn't fully control the linking of. If your application code needs to setup an OpenSSL socket, and then pass that socket to a service library, and the service library uses OpenSSL A.B.C-k and you use A.B.C-l, maybe that works, maybe it doesn't; if it doesn't, that's a heck of a problem to debug. Of course, it's even worse if you're not on the same minor version or across major versions.
While I'm picking on OpenSSL, because it's caused me (and others) a lot of grief, this kind of thing comes up with lots of libraries.
Containers are just a very wasteful way to do static linking.
Also, nothing stops you from putting your statically linked go app in a container which can then use e.g. kubernetes or nomad for horizontal scaling.
Yeah. It's a bug in the culture, really, and culture is much harder to change than software.
> problem when you share data with code you didn't fully control the linking of
Yeah, the data model needs to be versioned too. It's impossible to pass data between applications of different versions without the possibility of a bug. The options I'm aware of are A) provide that loose-abstraction-API and hope for the best, or B) provide versioned drivers that transform the data between versions as needed.
A is what we do today. B would be sort of like how you upgrade between patches, where to go from 6.3.1 to 9.0.0, you upgrade from 6.3.1 -> 6.4.0 -> 7.0.0 -> 8.0.0 -> 9.0.0. For every modified version of the data model you'd write a new driver that just deals with the changes. When OpenSSL 6.3.1 writes data to a file, it would store it with v6.3.1 as metadata. When OpenSSL 9.0.0 reads it, it would first pass it through all the drivers up to 9.0.0. When it writes data, it would pass the data in reverse through the data model drivers and be stored as v6.3.1. To upgrade the data model version permanently, the program could snapshot the old data so you could restore it in case of problems. (Much of this is similar to how database migrations work, although with migrations, going backward usually isn't feasible)
This isn't a server, it's something that people install on their machines and are free to upgrade in whole, or in parts (upgrade just a single dynamic library to fix a bug), or not at all.
My solution was to use a "COM-inspired" versioning. I defined an interface-based ABI. Not a function-level versioning as suggested here, it's more coarse than that, but it was enough. See my comment here [0].
That breaks as soon as you have a diamond situation. A depends on B and C, which each depend on D. Often you need that to be the same version of D. If B and C are allowed to depend on different versions of D, you get worse problems. There are languages that have tried this model and it has its advantages, but IME the cure is worse than the disease.
> The basic concept for this already exists in glibc as you can ship version-specific functions and link/call a version-specific function. But literally no one uses it.
It doesn't actually work properly, mainly because linux package management sucks. No-one cares about backwards compatibility on linux because you're either using a rolling release distro or you're using a traditional distro that will do all your compatibility checking and versioning for you and upgrade the global version of everything when they're good and ready. Windows has much better support for this kind of thing and that's why you don't really hear about "DLL hell" anymore.
This isn't the biggest thing holding back software development, not by a long shot. It's a side issue that we tweaked and can keep tweaking. But being able to version libraries and functions doesn't work, at least not alone; it breaks the idea of having a platform that mostly-independent components can build on.
Yeah :( If B1 and C2 are installed, they might depend on D1 and D2, respectively. But luckily, A can be built against B1 and C1, which can both depend on D1. Or, A can depend on B1 and C2, and B1 will talk to D1, and C2 will talk to D2; when B1 talks to C, it will talk to C1, because that's the version B1 was built/tested against; if C2 talks to B, it will talk to B2. However A was built and tested, the versions of functions that it used are what will be called. (There may need to be some deeper, ugly static mapping of function deps from compile time, but essentially it should be possible for every version of an application to record what dependencies it used and those dependencies, and for function calls to automatically download and call the correct dep, up/down/across the hierarchy)
The data model is the really sticky wicket, but I think migrations provide a path to deal with it, or just a loosely coupled API.
> mainly because linux package management sucks
Yes that too :) But if apps used versioned functions then package management wouldn't suck so bad!
> This isn't the biggest thing holding back software development
It's what keeps us from changing software frequently/easily, which I think is the biggest problem limiting advancement of the discipline. Changes cause bugs and bugs cause a waste of time, and fear of bugs makes us jump through hoops and keep us from making the changes we need to move other things forward. People are trying to work around it by vendoring deps or statically linking but it's a bad hack because the app & data interfaces are still volatile.
For example, all Cloud technology is currently mutable. An S3 bucket as a whole concept cannot be snapshotted, changed, and then reverted automatically. That's a limitation of the service and the API. Making it immutable requires redesign, which would probably break existing stuff. Every single cloud service is like this. It will take 10+ years to redesign everything in the Cloud to be immutable. But imagine if we could just make it immutable tomorrow, and nothing would break, because old software dependent on S3 would keep using the old version! Make any change you want at any time and ship it right now and nothing will break. That's the future that nobody can imagine right now because they haven't seen it. Just like they haven't seen an OS with only primary storage.
Wishful thinking. The main issue with Linux package management is the fragmentation between distros and the fact that human volunteers can't keep up with upstream.
That only works if B knew about C, or if there's a version of D that there are releases of both B and C that were built against. Neither of these can be relied on, especially when you bring in E and F and all the other dependencies that a real application will have (e.g. maybe B updated E before updating F, whereas C updated F before updating E).
> The data model is the really sticky wicket, but I think migrations provide a path to deal with it, or just a loosely coupled API.
The data model is the root of the problem, if you can't fix that you can't fix anything.
> For example, all Cloud technology is currently mutable. An S3 bucket as a whole concept cannot be snapshotted, changed, and then reverted automatically. That's a limitation of the service and the API. Making it immutable requires redesign, which would probably break existing stuff. Every single cloud service is like this. It will take 10+ years to redesign everything in the Cloud to be immutable. But imagine if we could just make it immutable tomorrow, and nothing would break, because old software dependent on S3 would keep using the old version! Make any change you want at any time and ship it right now and nothing will break.
But as long as there are still applications that mutate them, your buckets aren't immutable! Effectively you're just introducing a new, incompatible cloud storage system that client applications have to migrate to - but that's something you could already have done.
The problem isn't depending on old versions versus new versions. The problem is getting consensus on the semantics between all parts of your system, and being able to depend on an older version of something just moves the problem around.
Say, you have a certain access sequence for locks that has to be obeyed by all functions in this library. Suddenly in some version, you want to add another lock or expose some API that does things with finer granularity. Suddenly the precondition for older code breaks, so there is no guarantee that users can sometimes call the old version of the API and sometimes call a newer version of the API.
I am not saying that the weird example I mentioned is a good way of programming, I just don't think that function versioning is the magic bullet to solving dependency hell. Intra-package dependency conflict is a difficult problem, neither the NixOS approach nor your approach can magically fix that.
If A and B are developed independently, but some day somebody makes A and B try to talk together, and they were never tested together, they could either 1. make a best effort to try to work and then die, 2. compare mutual dependency trees and try to walk the trees to find versions of themselves (A and B) that work with the same version of a dependency (C), or 3. simply not run at all because they don't know what version of each other to use.
In theory you could use CI/CD to build every version of every app against every version of every dep, store them all in a remote registry, so that whenever some weird combo of apps wanted to use something, they could look up a version of that app built against the right dep.
For using CI/CD to build every version of every app against every version of every dependencies, even if we just ignore the problem of combinatorial explosion, some software may be abandonware and some may be entirely new. The set of versions of dependencies that work with them may not even overlap, so the 'correct' combination may not even exist.
I think the function versioning approach is just a way to maintain backward compatibility, it is not required for backward compatibility, nor enough for backward compatibility.
Edit: and worse, that's introducing plugin like problems in code that does not even use plugins.
Next idea?
All this while Rust, go, and all the new kids on the block use only static linking. Because static linking is a great idea.
If you can update a library, you can also update all other software, and usually from the same source.
Then someone decided to call it Docker.
No, it's more that they were designed by entities that distributed their software using static linking.
¹ via forcing the dynamic linker to eagerly resolve some symbols, global constructors, etc. - see https://github.com/rust-lang/rust/issues/73917#issuecomment-... for some recent discussion
² such as exporting a single base symbol per dylib (with a hash of code/data contents in the symbol name, for integrity), and statically resolving imports (to constant offsets from that base symbol), when linking object files into dylibs/executables - because of how position-independent code indirects through the "GOT", in practice this would mean that the dynamic linker would only need to lookup one symbol per dependency .so instead of one per import (of which there can be hundreds or thousands)
Also, "dynamic linking" in Rust was never really "dynamic" or about "drop-in replacement to avoid rebuilds", it was only about the storage reuse of "shared objects", with the "late-binding" semantics inherent in dynamic linking a negative, not a positive.
For example, rustc, rustdoc, clippy, miri, etc. are all relatively small executables that link against librustc_driver-*.so - on a recent nightly that's 120MiB, with only static linking that'd be half a GiB of executables instead. The relationship is "one .so shared between N executables" not "one .so per library". Also, if we could remove runtime symbol lookup but keep the separate .so (like ² above), we totally would.
---
At the same time, Rust's continued evolution depends on being able to (almost) "never ask for permission" when changing internals that were never guaranteed to be one thing or another.
Rust recently fixed most platforms to use simpler and more performant locks based on futex(-like) APIs (see https://github.com/rust-lang/rust/issues/93740 and PRs like https://github.com/rust-lang/rust/pull/95035), and this meant that e.g. Mutex<T> has a 32-bit (atomic) integer where a pointer used to be.
An even more aggressive change was decoupling Ipv{4,6}Addr/SocketAddrV{4,6} from the C types they used to wrap internally (https://github.com/rust-lang/rust/pull/78802) - that one actually required library authors to fix their code in a few cases (that were very incorrectly reinterpreting the std types as the C ones, without this ever being officially supported or condoned).
Imagine trying to do that in C++. The stories I keep hearing from people involved in the C++ standard are tragic, more and more otherwise-uncontroversial improvements are being held back by poorly-motivated stubbornness around ABI stability. Large-scale users of C++, like Google, have long since migrated from C++'s standard library to their own replacements (e.g. abseil) for many of their codebases, and I kept hearing Google were "forking C++" (over the frozen ABI debacle specifically), long before it came out as "Carbon".
---
The technology to coherently³ avoid today's rebuild-the-world cost in compiled languages⁴ isn't here yet IMO.
³ as in, guarantee identical behavior to the full rebuild from scratch, without introducing inconsistencies that could cause logic errors, memory corruption, etc.
⁴ some/most C libraries of course being the biggest exception - and while it's possible to intentionally design libraries like this in any language that can do FFI, it's fundamentally antithetical to "zero-cost abstractions" and compiler optimizations - nowadays the latter are forced even on otherwise-patchable C dependencies via LTO, in the interest of performance
What I'm referring to is incremental recompilation with the following properties:
...
[oops, hit comment size limit! I'll post the rest separately]
What I'm referring to is incremental recompilation with the following properties:
1. automatic correctness
` - that is, a compiler change not explicitly updating anything related to dependency tracking should at most result in conservative coarser-grained behavior, not unsoundly cause untracked dependencies
` - manual cache invalidation logic is a clear indicator a compiler isn't this - e.g. this rules out https://gcc.gnu.org/wiki/IncrementalCompiler (maybe also Roslyn/C# and the Swift compiler, but I'm not sure - they might be hybrid w/ automated coarse-grained vs manual fine-grained?)
` - the practical approach to this is to split workload into work units (aka tasks/queries/etc.) and then force information flow through centralized "request"/"query" APIs that automatically track dependencies - see https://github.com/salsa-rs/salsa for more information
` - research along the lines of ILC (Incremental Lambda Calculus) might yield more interesting results long-term
` - outside of a compiler, the only examples I'm aware of are build systems like tup (https://gittup.org/tup/) or RIKER (https://github.com/curtsinger-lab/riker) which use filesystem sandboxing (FUSE for tup, ptrace/seccomp-BPF for RIKER) for "automatically correct" build dependencies
2. fine-grained enough
` - at the very least, changes within function bodies should be isolated from other bodies, with only IPOs ("interprocedural optimizations", usually inlining) potentially introducing additional dependencies between function bodies
` - one extreme here is a "delta-minimizing" mode that attempts to (or at least warns the developer when it can't) reduce drift during optimizations and machine code generation, such that some fixes can end up being e.g. "patch a dozen bytes in 200 distro packages" and get quickly distributed to users
` - but even with history-agnostic incremental recompilation (i.e. output only depends on the current input, and past compilations only affect performance, not other behavior), function-level incremental linking (with one object file per function symbol) could still be employed at the very end, to generate a binary patch that redirects every function that grew in size, to additional segments added at the end of the file, without having to move any other function
3. propagating only effective "(query) output" changes
` - also called "firewalling" because it blocks irrelevant details early (instead of redoing all work transitively)
` - example 1: editing one line changes the source byte offsets of everything lower down in the file, but debuginfo doesn't need to be updated since it tracks lines not source byte offsets
` - example 2: adding/removing explicit types in a function should always cause type checking/inference to re-run, but nothing using those type inference results should re-run if the types haven't changed
4. extending all the way "down" (to machine code generation/object files/linking)
` - existing optimizing compilers may have a hard time with this because they didn't design their IRs/passes to be trackable in the first place (e.g. a LLVM call instruction literally contains a pointer to the function definition, with no context/module/pass-manager required to "peek" at the callee body)
` - can be theoretically be worked around with one object file per function, which https://gcc.gnu.org/wiki/IncrementalCompiler does mention (under "Code Generation" / "Long term the plan is ..."), but that alone isn't sufficient if you want optimizations (I've been meaning to write a rustc proposal about this, revolving around LLVM's ThinLTO having a more explicit split between "summary" and "full definition")
5. extending all the way "up" (to lexing/parsing/macros)
` - this is probably the least necessary in terms of reducing output delta, but it can still impede practical application if it becomes the dominant cost - AFAICT it's one of the factors that doomed the GCC incremental experiment ("I’m pretty much convinced now that incremental preprocessing is a necessity" - http://tromey.com/blog/?p=420)
` - outside of "rebuild the world", this also makes a compiler much more suitable for IDE usage (as e.g. a LSP server)
My experience is mostly with the Rust compiler, rustc, whose incremental support:
- started off as coarse skipping of generating/optimizing LLVM IR
- has since evolved to cover 1./2./3.
- while many passes in the "middle-end" were already both "local" and "on-demand", sometimes allowing incrementalization by changing only a dozen lines or so, 4. is indefinitely blocked by LLVM (at best we can try to work around it), and 5. (incremental "front-end") has been chipped at for years with several factors conspiring to make it orders of magnitude more difficult:
` - macro expansion and name resolution being intertwined (with the former mutating the AST in-place while the latter being a globally stateful algorithm)
` - "incremental front-end" used to be seen as essential for IDE usage and that notion started falling apart in 2018 or so (see rust-analyzer aka "RA" below, though RA itself is not itself directly responsible, nor do I hold it against them - it's a long messy story)
` - this work (without the IDE focus, AFAICT) has mostly driven by random volunteer work, last I checked (i.e. without much "official" organization, or funding, though I'm not sure what the latter would even be)
- its design has been reimagined into the "salsa" framework (https://github.com/salsa-rs/salsa - also linked above)
` - most notable user I'm aware of is the rust-analyzer (aka "RA") LSP, which hits 1./2./3./5. (4. isn't relevant, as RA stops at IDE-oriented analysis, no machine code output) - without getting too into the weeds, RA is a reimplementation of "Rust {front,middle}-end", and ideally eventually rustc should be able to also be just as good at 5. (see above why it's taking so long)
I am not aware of any other examples of industrial compilers having both 1. and 2. (or even academic ones but I suspect some exist for simple enough languages) - every time someone says "incremental compilation? X had that Y decades ago!", it tends to either be manually approximating what's safe to reuse (i.e. no 1.), or have file-level granularity (i.e. no 2. - the "more obviously correct" ones are close to the "separate compilation" of C/C++/many others, where a build system handles parallel and/or incremental rebuilds by invoking the compiler once per file), or both.
While 2./5. are enough to be impressive for IDE usage all by themselves, the interactive nature also serves to hide issues of correctness (lack of 1. may only show up as rare glitches that go away with more editing) or inefficiencies (lack of 3. leading to all semantic analyses being redone, may still be fast if only requested for the current function).
Another thing I haven't seen is distro build servers talking about keeping incremental caches around (and compiling incrementally in the first place), for builds of Rust "application" packages (most of them CLI tools like ripgrep, I would assume) - it's probably too early for the smaller stuff, but a cost/benefit analysis (i.e. storage space vs time saved rebuilding) would still be interesting.
Pinning a nightly and installing the "rustc-dev" rustup component are both necessary, the former because internal APIs of rustc aren't stable, but it's a supported usecase.
Both clippy and miri are developed out-of-tree like that, and sync'd using `git subtree` (or `git submodule` but we want to move everything to subtree).
This is trivially disproven:
$ ldd /usr/bin/rustc
linux-vdso.so.1 (0x00007ffd3de9b000)
librustc_driver-e4385bf403336f76.so => /lib64/librustc_driver-e4385bf403336f76.so (0x00007ff832a00000)
libstd-d371b708c754b3bb.so => /lib64/libstd-d371b708c754b3bb.so (0x00007ff832886000)
libc.so.6 => /lib64/libc.so.6 (0x00007ff832600000)
libLLVM-14.so => /lib64/libLLVM-14.so (0x00007ff82be00000)
[...]
Not only does rustc dynamically link against rust's "std" library, but also parts of rustc are implemented in a dynamic library ("librustc_driver").The only reason projects written in Rust (other than rustc itself) don't use dynamic linking to other Rust code yet, is that there still isn't a stable ABI for Rust code (for good reason: that allowed them to introduce not only reordering of struct fields and niche optimizations, but also passing and returning slices as a pair of registers instead of a pointer to a two-word structure in the stack). I fully expect that, as the language matures, a stable (possibly opt-in) ABI will be introduced, together with freezing some of its standard library (for instance, the contents of the Vec structure, and how its inline functions work), allowing for dynamic linking against Rust's "std" (and as a consequence, passing Rust types between separate dynamically linked modules compiled with different releases of Rust).
(As an aside: back when C was the "new kid on the block", it also used only static linking, dynamic linking of C code came later.)
You should analyze an executable created by the rust compiler instead.
AFAIK only glibc and libgcc are linked dynamically in a program written in rust, and there's a way to statically link these.
What you are complaining about is ABI stability between versions of Rust but you are hiding that by arguing steveklabnik is violating the spirit of your secret ideals.
Nit: 100% of rustc is found within librustc_driver-*.so (I mention it in https://news.ycombinator.com/item?id=32329062 - which also touches on stable ABI).
`rustc` just calls into the .so without any custom configuration: https://github.com/rust-lang/rust/blob/e141246cbbce2a6001f31...
(but rustdoc/clippy/miri all have additional configuration, passes, etc. specific to them)
Also, if you look at the `rustc` executable's size:
$ du -bh ~/.rustup/toolchains/nightly-*/bin/rustc
17K /home/eddy/.rustup/toolchains/nightly-2018-01-01-x86_64-unknown-linux-gnu/bin/rustc
2.9M /home/eddy/.rustup/toolchains/nightly-2021-12-02-x86_64-unknown-linux-gnu/bin/rustc
2.9M /home/eddy/.rustup/toolchains/nightly-2022-01-13-x86_64-unknown-linux-gnu/bin/rustc
2.7M /home/eddy/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/rustc
17K is what it should be, I'm not sure where the extra 2.7M are coming from - my current suspicion is that it's entirely statically linked jemalloc, which I guess we didn't want to impose on all users of rustc_driver, but it might be worth doing it anyway.As for rebooting not being a thing, the IBM AS/400 and its offspring (currently named IBM i) have been doing it for decades now. They are very alien when compared to Unix, but still quite cool. And extraordinarily mainframe-like reliable.
There's a reason they cost a small fortune.
Dependency hell isn't just dealing with incompatible dependencies. It has another side of forcing upgrades when they are needed for very, very good reason.
There could be a dialog that warns the user and offers to disable functionality.
We will have to eventually do an accurate permission system anyway.
Out-of-support branches aren't suddenly going to start being supported again, just because you've changed how dependencies are managed.
Lets say there are 10 versions of my function. A critical vulnerability is discovered, and I publish a fix in version 11. I hire someone to backport the fix to all 10 historic versions of the function.
Now I have 21 distinct versions of the same function.
Oh no, another critical vulnerability has been discovered!
I fix it in version 22, and hire a team to backport the fix to all 21 other versions.
Now I have 43 versions of the same function!
Oh no...
Sure, you could be a bit more pragmatic about which history nodes you backport to, but it's still fundamentally O(n^2). It's hard enough to manage backports at a whole-project level, let alone per-function.
But then it wouldn't be the old version of the function any more, but a brand new function. If you allow arbitrarily changing 'old' function then you just lost all the guarantees of stability and reproducability that this approach was supposed to offer.
Frankly, what we actually need is a unified package format like we have for containers rather than specific micro optimizations that nobody really asked for. If gradle, npm, pip, cargo, pacman, apt all speak the same package format you only need one registry for all languages. After that you can think about building a unified package manager. After that you can think about a unified build system.