Open question: what would you like to see from a Mojo package manager?

How do you envision the future of Mojo packaging? Feel free to add your thoughts, opinions, suggestions, and more to this thread.

3 Likes

What we have right now

Right now we have pixi as the build and packaging tool. Packages are conda packages, and our central repository is over at modular-community. To publish a package you have to create a rattler-build recipe to contribute to the modular-community repo, duplicating a lot of the things already in your pixi.toml.

Technically we also have decentralized packages as long as the packages use the pixi-build-mojo backend, allowing you to just rely on git paths for dependencies.

There are several pain points with this, in my view:

  • No easy single command method for creating new projects
  • No ubiquitous mechanism for running tests
  • Uses a totally separate rattler-build system, and separate repo, for publishing packages

Given those deficiencies, I think there are two primaries areas for discussion:

  • Ergonomics
  • Conda

Cargo-like ergonomics

In general, I think basing the design around Cargo is a good starting point. And by that I specifically mean the approach that Cargo has taken to progressive complexity, and its on-boarding process.

Common things should be easy. Starting a new Mojo project should be as simple as <tool> new, adding a dep should work with <tool> add awesome-dep, tests should run with <tool> test, etc. But all the nobs should be available to doing complex things like package patching, git deps, local path deps, etc.

Most importantly, there should be minimal extra steps between a properly formatted project that works with the tooling, and publishing that package so others can use it.


To Conda or not to Conda

The polyglot nature of a lot of Mojo projects, combining Python / Mojo / C, etc really makes Conda a pretty attractive manager, not to mention that it is already well established.

Having Mojo use conda as the package layer makes it really easy to pull Mojo projects into existing datascience and ML workflows without adding another tool into their stack, or forcing maintainers to publish in multiple places.

I think I’d lean toward continuing to use conda as the backbone of the package system, or at least remaining highly compatible with it.


Assorted specifics (repeating a lot of the above):

  • Per-project environments
  • Central package repository
  • Ubiquitous tooling across the ecosystem for build and test commands
  • Path and Git deps
  • Ability to patch deps
  • Publishing a package being a simple next step requiring limited extra info to do
  • Some degree of compatibility with Python
  • Ability to install / distribute binaries (i.e. cargo install)
  • Written largely in Mojo
5 Likes

I’d love to see a potential package manager to be built on top of Nix (mojo in nixpkgs, projects with flakes).

  1. Guaranteed “if it works on my machine it works on your machine”.
  2. Pure functional; if a dependency is missing in the flake the build will fail even if the package is installed on your local system.
  3. Easy to read package descriptions.
  4. When upgrading, nix-review tests all dependencies and reverse-dependencies (who else has that?).

I don’t find the CMake/cargo/conda/pypi situation appealing. CMake at least has Dependency Providers that have the potential to solve this mess once and for all.

I think my main pain points are distribution and lack of good defaults for pixi.
the conda ecosystem is suitable for mojo because it works really well with multi-language projects so the mojo package and the python bindings can be built at the same time.

the main problems is that to distribute a mojo package one has to make a recpie, open a PR, wait for approval. in the Pypi world, this can be automated so packging and distrbution can be set up once.

The other pain point is that pixi is a general purpose package manger, builder. it has no specific support, ergnomics for mojo. in addition, the build feature is also not quite mature and pixi.toml with packages has a lot of unintuitive duplication.
one solution is if the pixi ecosystem would allow plugins to certain languages so a mojo plugin for pixi could be a good solution for build and distributing mojo project.

1 Like

I would second many of the points made above. One thing that I particularly enjoy about pixi (and would like to keep) is the multi language support, which I think is key if Mojo should stay closely integratable with Python.

What follows possibly comes over a bit grumpy. Please read it in the spirit of appreciating and valuing what the Mojo team are doing, and wanting the best future for the language :slight_smile:

I’m uncomfortable with linking Mojo too closely to Conda. As a new user, the connection put me off using pixi: it felt like an imposition of tooling choices that I wasn’t happy with.

My thinking is probably coloured by being a systems programmer first and foremost. The simplest use case of Mojo is just compiling an executable or a shared/static library, for a given hardware target. That doesn’t require a distinct project environment, a Python venv, or anything of the sort.

So, I think it’s important that any packaging tool operates a “pay only for what you need” principle. In other words, by all means, let’s make it easy for people to build and publish conda packages, or to install a Mojo package into a specific Python venv, or to generate Python bindings for a Mojo library. But let’s make those options that a user can request, rather than requirements or impositions. If I’m writing purely Mojo code, and depending on purely Mojo packages, I shouldn’t need to care about any broader ecosystems.

Honestly, without more detailed context (I appreciate there may be needs I’m not thinking of), I already find it too much that installing the Mojo toolchain currently seems coupled to installing into a Python venv of some kind. (I currently install it using pipx, which seems the least intrusive way of doing so.)

In short, I think it would be good for any package management solution to carefully decouple and delineate what is needed to write, build, test, and manage purely Mojo projects from what is needed for wider ecosystem integrations.

2 Likes

Features I like:

  • Being able to “jump-to-source” in vscode, that brings me to the library code, I can modify the library code and then re-run my code again to see what’s different.
  • Being able to have two versions of the same dependency at the same time for a project. For example if I use a library X v1.3.4 purely internally to do something, I don’t want to force my users to use X v1.3.4 in their app. They should be free to install another version if they want.
4 Likes

For the purposes of this discussion, I’m going to treat a built system and a package manager as one concept because I think they need to be heavily co-designed even if they are not the same thing. Some parts of this may not strictly be build system features, but may be library features with a level of build system support.

Users

I think we’re going to get a few different groups of user, and they’re going to want different things. One programmer may belong to multiple groups at a time.

Group 1: “KISS” Mojo Programmers

This group writes code in pure mojo, from main all the way to CPU and GPU kernels, often alone or in small teams, and their laptop has all the compute they need. Whether they’re just learning Mojo with a toy project or find Mojo to be sufficient for all of their needs, they want to focus on writing code, not infrastructure and dependencies.

Desires

  • Easy project setup - They want to be able to generate the scaffolding for a Mojo project easily, and are fine letting a Ruby inspired “convention over configuration” give them a folder with source code, and a config file that has a lot of the information a package registry would want already filled out. This includes stamping out a tests directory, adding .gitignore files, and maybe including configurations for common CI platforms to build/package the project.
  • A single tool - They don’t want to use different tools for getting dependencies, setting up the project, compiling and testing.
  • Low friction libraries - Pulling in a new library should be as easy as possible.

Group 2: “Faster Python”

This group is writing Mojo to accelerate parts of Python code. Whether this is via MAX or a custom python library written in Mojo, for them Mojo’s primary purpose is to do what Python can’t.

Desires

  • Easy integration into existing python projects - These users want minimal distance from mojo new to import way_faster_mojo_function in their python code. This might mean a command to scaffold out a python project
  • Easy package creation - A “build” which outputs a python library which is compatible with as many options in the python ecosystem as possible. This may mean producing rattler build files, conda environment files, handling for Python build tools and popular Python package managers which don’t support modern python standards.
  • Knowing hardware requirements - When libraries have specific hardware requirements, they want to know about that before they deploy. Ideally, the package manager should flag new libraries or library updates which narrow the supported hardware for a project.

Group 3: Transitive Users

This group is people who are using Mojo via a python library of some sort. This may be users of MAX who work purely from the python API, or users importing some accelerated processing which happens to be in Mojo.

Desires

  • “What’s Mojo?” - Many python developers know the sinking feeling of seeing a C++ or Rust compilation error when trying to install or set up a project and we should try to avoid Mojo causing the same problem.
  • No “package version hell” - Mojo-based packages should not require the entire world to adopt a given version of the Mojo compiler before they can be used (see issues with new CUDA versions). This means that this group implicitly desires build-time dependencies to help keep libraries which use different versions of Mojo but don’t interact compatible.
  • Compatibility with GIL Python - Many libraries are unlikely to ever leave GIL python
  • Compatibility with NoGIL Python - Multi-threading is quite useful after all.
  • “It Just Works” - Mojo packages, where possible, automatically adapt themselves to the user’s environment (Environment Inspection).

Group 4: Kernel Developers

This group makes their own kernels, be they for AI, HPC, or for more exotic usecases, and they only have a single device or family of devices they care about.

Desires

  • Minimal dependencies - A NVIDIA GPU kernel developer doesn’t want to download ROCm and oneAPI.
  • The latest and greatest from vendors - The freedom to require the latest and greatest vendor tools, potentially even from special builds provided by the vendor, to get the maximum possible performance. This may also require telling users to update their drivers to use a library. (Custom dependency failure messages)
  • Driver version flexibility - A widely portable library written by a vendor’s software team may want to support as many driver versions as possible, automatically disabling feature flags in environments with old drivers. (Environment Inspection, Feature Flags)
  • Accelerator dependent feature flags - The ability to enable or disable feature flags based on the capabilities of a target device.

Group 5: Generic Kernel Developers/“Accelerated Library” Developers

These developers want things to generically “go fast”, using whatever hardware is at hand, and ideally want to support a wide range of users.

Desires

  • Environment-aware testing - An easy way to make test not run the 8xB200 tests on someone’s laptop.
  • Knowing hardware requirements - Similar to the “Faster Python” developers, these developers care about knowing the hardware requirements of their project and making sure they don’t exclude users unintentionally.
  • Everything general kernel developers care about - These developers also care about driver versions and hardware dependent feature flags.

Group 6: Airgapped System Developers

This group of developers is making use of airgapped systems. They might be using an HPC cluster which is airgapped to allow security to get out of the way of performance, or might be working on sensitive software for a government or large company.

Desires

  • Packages not over the internet - These developers have a hard requirement on a way to handle packages that does not require a connection to a centralized authority.
  • Private package indices - Carefully audited libraries can be allowed inside of the security perimeter, where they can be uploaded to a private package index for more typical use. This requires that the package index can be audited as well, and that a package index can operate totally independently. These indices should also require a minimal amount of software to run (no hard dependencies on cloud object stores, kubernetes, etc).
  • Package and package dependency overrides - These developers need the ability to replace any git URL dependency with a copy inside of their security perimeter.
  • A reasonable “build the universe from source” story - As much as possible, this group wants to build from source on known secure systems.

Group 7: Developers of Large Libraries.

This includes Modular, but also any other organizations with large “ecosystem important” libraries (think C++'s Boost or Rust’s Tokio). This group is going to have a lot of complexity, but I don’t think there’s a way to avoid that complexity.

Desires

  • Package namespaces - These developers need the ability indicate to users that a group of libraries belongs to them, and to prevent “name squatting” of package names, like max_ccl (instead max/ccl or modular/max/ccl) or mojo_lsp (instead mojo/lsp). This is useful for users as well, since it helps indicate “trust domains”, which help reduce concerns about “too many dependencies” if users can see that they only need to trust, for example, owenhilyard (me on github), to get owenhilyard/mojo_ipv4, owenhilyard/mojo_ipv6, owenhilyard/mojo_icmp, etc. This gives a similar logical separate as C++'s boost currently uses, but enables more granular packages for more separate versioning and to enable users to depend on a minimal amount of code, instead of focusing on the number of entries in the dependencies list.
  • The ability to transfer packages and leave an “alias” - Sometimes, your project gets bigger than you thought, and it becomes time to transfer it to a github org so it can survive something happening to you. There needs to be a way to do this without large disruptions to users, but users should be poked to move to the new name.
  • Package signing - As a library becomes more important, it becomes more of a target for supply chain attacks. At some point, trusting the package repository is no longer an option, and these users will desire a way to sign packages so that users can validate the package repo has not modified them (note that only the end user should do this, see the package override use-case for airgapped development). I don’t know a good way to handle this automatically at this time due to bootstrapping issues (how do you verify that the shared public key is actually form the devs?). The repository should also host a revocation list.
  • Granular API permissions - The API for the package repository needs to have granular permissions. For instance, a token which grants “yank” (remove from registry unless present in a lockfile, warn everyone using it, usually done for security reasons or serious bugs) permissions, “update public signing key”, “add maintainer” or “generate new API key” permissions needs to be much more carefully guarded than a “stage package” (do everything involved in pushing it to the registry except making it public, which can be a useful security feature to keep compromised pipelines from publishing malicious package versions).
  • Precompiled build dependencies (The serde problem) - A project which needs to execute code at build time, for instance to do custom device detection, will require running the compiler or interpreter. However, even getting the interpreter going can be slow in some cases, so it’s beneficial to be able to provide either a set of binaries or portable execution format can noticeably reduce the compile times of the entire ecosystem.
  • Minimum Supported Mojo Version (MSMV) - Inevitably, people will want to stick on older versions of the compiler for stability, or because someone hasn’t updated their code yet, or for some other reason. Large libraries tend to support this behavior by not taking advantage of every new feature in the language, for instance by using C++ 17 instead of C++ 23, allowing those with older toolchains to continue to use new versions of the library. This information should either be automatically inferred by the compiler (best case) or included as metadata in the package definition. This can be incorporated into the process to update dependency versions, which of course needs a “try it anyway” override.
  • Feature flags - Many people don’t need everything in a library, and some feature flags, such as “assume that there’s a global allocator” may be in wide use, but need to be possible to disable in order to support more exotic environments.

Group 8: Developers using Python to “fill in” missing Mojo capabilities or ecosystem

This group of developers will have mostly Mojo code, but a few python dependencies.

Desires

  • Python dependencies without a cost to Mojo ones - Taking on python dependencies should not make the experience of using Mojo dependencies worse
  • Packaging python alongside a mojo application should be easy - Ideally, python dependencies can be baked into the binary.

Group 9: Developers depending on other systems languages

This group of developers relies on C, C++, Rust, Zig, Fortran, etc libraries for some important functionality. These libraries are often large, battle tested, and impractical to replace quickly (if at all).

Desires

  • Indicate native dependencies in the manifest - If a library depends on zlib, that information should be available in a machine readable way, potentially to aid in automated installation.
  • Build code as part of building the Mojo library - When possible, distributing these other languages as source should be preferred. This can help avoid many of the issues the python ecosystem has faced with packages being “colored” by precompiled libraries with unstable ABIs. Ideally, integrations for common build systems could ease this process. Sandboxing may be desirable here.
  • Specify linker flags - If libraries are distributed as binaries, then providing linker flags is mandatory.
  • Binding Generation - It would be really nice to be able to generate bindings to libraries as part of a build.

Group 10: Developers integrating Mojo into existing systems language codebases

Mojo’s access to accelerators, or it being generally good for portable SIMD, may tip the scales in some codebases which are currently in C, Fortan, or C++ that ho have performance as their north star.

Desires

  • Integration with existing build tools - A 30 year old library isn’t going to update to pure Mojo overnight. Mojo needs to be able to output things in a format that are reasonable for older build systems, such as:
    • cmake
    • meson
    • autotools (yes, really)
    • msbuild (eventually)
    • Bazel
  • Binding Generation - As much as possible, low-level bindings should be automatically created in both directions.
  • Cross language LTO with Mojo - Performance as a guiding principle means cross language LTO and cross-language inlining isn’t optional.

Group 11: Library developers with closed source redistributable dependencies.

This group of developers, while permissively licensing the code, don’t provide all of it as source code, either because they don’t want to or because they’re getting the binary from someone else who lets them redistribute it.

Desires

  • Native dependency requirements - If the closed library is linked against a particular version of musl or glibc, it might not be safe to use any other version.
  • Requirements for particular hardware in the manifest - The blob may contain a bunch of PTX.

Group 12: Users of Code Generation Tools

These developers have code generation tools. These might be binding generators for other languages, API client generators (Swagger, SOAP, gRPC) binary format definitions (ASN.1, CSN.1, TSN.1, Protobuf), or even descriptions of the skeleton of entire programs (UML, ITU-T’s SDL).

Desires

  • First class support for code generation - Avoiding the need to have multiple layers of build steps
  • LSP servers to be able to inspect generated code - This may mean that the LSP server needs to run build steps to produce the necessary Mojo files.

Summary

Overall, I think that users collectively want:

  • Arbitrary metadata in the package manifest for other tools to consume
  • Integration with every other build system in both directions
  • The ability to run arbitrary code at build time
  • Either the sandboxing (for dependencies and libraries) of Bazel-like build systems or easy access to system libraries.
  • Sandboxing for security

How do we accomplish this?

This is a fairly tall order list, but I think it can actually be done. Zig’s build system provides a way to wrangle a lot of complexity like this, but it has a few problems. First, you can’t easily use libraries, which causes a lot of headaches. Second, it’s not sandboxed, which means there are security concerns. Third, and this is probably the easiest to fix, it’s not great about incremental compilation.

Mojo has an interpreter, so as long as we can do something which is more or less the equivalent of mojo repl < build.mojo, setting up libraries and dynamically loading them in later lines should be possible, although we may need a way to modify the library search path inside of the interpreter. Sandboxing can be done via having a mode in the interpreter which blocks inline_assembly, external_call, access to a config file which specifies what “permissions” the build.mojo file has (for obvious reasons), and anything else we determine to be a problem from a security standpoint, at least by default. When a repo is opened, the user can be prompted for whether they want to execute the build.mojo file with a list of the permissions it requests and if they say yes, the path and hash can get added to a file for the LSP to reference later. Combined with a few primitives, this should be enough to let people start to explore the space in a mostly compatible way. Whether or not to make this a lazy system is also up for debate, I can see advantages to it, but it does make debugging a lot easier to make it eager. Maybe we can have both here, but I think making the lowest level primitives eager makes sense, and then we can have higher level types which are lazy that live on top of that.

Over time, we can slowly move capabilities (ex: cmake integration) we find are widely used into being shipped with the stdlib, but this gives us a mechanism to force a given build system capability to prove its usefulness before we consider integrating it in the vast majority of cases.

One of the other benefits this may give us, in time, is that experimenting with something akin to a Bazel server should be a lot more reasonable since we are just running code, so the “build” command in an IDE can just be an RPC to something spawned from the build file.

Primitives

I think that, to start with, these should be sufficient

  • build_module - More or less mojo package
  • build_exe - mojo build --emit exe
  • build_library - mojo build --emit=shared-lib
  • build_object - mojo build --emit=object

We’ll probably want to expose fetching libraries from a registry too, but that can come later.

Making “KISS Mojo Programmers” Happy

I think that, by default, many projects won’t require much beyond write access to a build directory , and either a package at ./src or a ./main.mojo. To start with, we can have downloaded packages live in build as well, so everything is nicely self-contained. This can be handled via boilerplate generation. We can look at using File.read to allow reading in something that looks more like a traditional Cargo.toml so that we can have an easy “add dependency” command.

2 Likes

Nix has fairly substantial issues with closed source dependencies which are expected to be present on the system, and Nix also has issues like not supporting Windows and not actually being able to figure out whether flakes are good. I think it’s a decent idea, but Nix is a very heavyweight dependency to ask people to take on in order to make use of Mojo, since you’d need a Nix build of glibc, which means you need to move the entire world inside of Nix. Bazel has a similar problem where it doesn’t play very well with projects which aren’t already in the Bazel ecosystem. Nix flakes also make using it as a build system rather difficult, since many people hardcode cflags.

I’d actually contest that. Conda is really, really bad as soon as you don’t have a binary repo to lean on. Many conda packages still haven’t adopted build definition formats and Conda is quite leaky as far as build systems go, with a lot of packages picking up accidental dependencies from the system.

I agree, this is a big issue. Not having Mojo specific defaults leaves Mojo at a disadvantage even when compared to Fortran.

I agree. Python venvs and Conda are largely workarounds for issues with python, and they cause a lot of headaches for IDEs (as we’ve seen with the LSP being unable to find the stdlib in many cases). Having easy access to the python ecosystem is nice, but that shouldn’t come at the cost of added complexity for pure Mojo projects, and certainly shouldn’t impose on projects which are already juggling 2/3 languages with their own ecosystems like Gnome is.

Strong agree, dependencies which aren’t part of the API of the library shouldn’t force users to use the same version. Python can’t really handle this properly, so it has to forbid this, but Mojo can use the same solution to this problem as Rust does, only yelling at the user if they use the wrong version of a type somewhere.

1 Like

@ahajha I’d appreciate some feedback from the platform team on this idea.

A lot going on here, I’ll try to summarize my thoughts, I am looking at this from the perspective of someone who would be heavily involved in whatever this shakes out to be.

First of all, just getting this out of the way, making a package manager or build system is hard. really hard. We started with the Modular CLI, eventually had our sorta-fork magic, and now we’re relying on off-the-shelf tools. I talk about the 80-20 principle a lot, where you get 80% of the benefit for 20% of the work (and vice versa for the last 20%), what we have now feels like an 80-5: Not doing anything has a huge benefit in that it allows us to outsource that work to teams and companies dedicated to making those things. My team, in its current state, is not ready to do much beyond implementing packages for various package managers.

With that out of the way, I do think there’s plenty of room for improvement. One thing that I personally dislike about the current state of affairs is platform detection: Wheels specifically, but also somewhat Conda, have no way of detecting what type of GPU you have (conda has __cuda, which is something but not as much as I’d like). Being able to install GPU-specific tools or prebuilt kernels would be a huge unlock for installation speed and size.

I have been very focused on the Python interop narrative personally, which means that Conda and Wheel support for Mojo is a huge boon, we can just exist in that ecosystem without having an entire custom toolchain for people to learn. Aside from simplicity of install files (which we could make wrappers for), I think we’re most of the way there.

Standalone Mojo support is definitely lacking. The problem as I see it is that each language has their own solution for this: Cargo, Zig, CMake, etc. Even if they’re really good (looking more at Cargo than CMake), the second you leave that language they tend to fall apart. It’s one reason I really like our Bazel usage here, since as I mentioned before making a build system is really hard.

I certainly don’t know what the end result looks like, but I doubt we can make everyone happy. There’s going to have to be some sort of “meet in the middle”. Even if we supported every package manager out there, we would end up with a fractured ecosystem because some people would make packages for some package managers and not others, and common libraries would end up in packaging hell. Supporting fewer package managers is a feature, so if we go that route we need to be picky.

There are certainly some small wins we could get though. Just shooting out some ideas, some which I have discussed with @owenhilyard before:

  • Bootstrap scripts for different project types (bazel/pixi/uv)
  • Upstreaming support into pixi or uv (The Prefix.dev people could be receptive to this)
  • Putting Mojo on conda-forge to avoid an extra channel (Having it on PyPI was a huge accomplishment in my eyes)
  • Working with the Conda and Wheel spec maintainers (make CEPs/PEPs) to improve those package ecosystems to make Mojo more of a first-class citizen

Anyways, those are my thoughts. If it were up to me, I would try to stretch how much we can do by wrapping existing things before we make our own.

2 Likes

I agree that the platform team can’t really be the ones driving all of this. Ideally, this can be a community thing done after the compiler opens up. My hope is that we can have the community drive integrations so that only build systems people care about get support, and then we can just have an API for everyone to put the “package graph” into which produces build system files for other build systems.

Basically UV (Cargo-like) - fast and general. But with Go’s UI. Go’s command-line tooling is really great; go fix, go test, go get, go mod, go fmt, go vet, etc.

This raises an important point about goals, because generally I experience cargo as a tool for fetching dependency sources to build as part of building one’s own code, whereas uv (like pip) is more about fetching prebuilt packages to install into a Python environment.

Of course, sometimes those packages will get built on the fly – anyone who’s tried to install numpy where there isn’t a prebuilt package will know this pain as everything slows down waiting on a big C build – but either way, what uv and pip do for Python is different from what cargo does for Rust.

You can use cargo to install things, of course, but that’s different from its typical use-case. And even with the cargo install command, you’re always fetching and building, not (as far as I recall) fetching a prebuilt package.

Just like uv, Cargo, and Bun, a unified ‘all-in-one’ CLI is essential for a great developer experience. I’d love to see a declarative configuration (like kivy) that even integrates with Mojo’s comptime capabilities.

It would be amazing if the package manager could pass optional parameters/metadata to the entry point, allowing us to detect dependency mismatches or forks or API breakages during compile-time rather than at runtime.