Skip to content

feature: MiniScript package manager #35

@JoeStrout

Description

@JoeStrout

This is something that could apply to both command-line MiniScript and Mini Micro (as well as possibly other environments, such as Soda). But the focus on this ticket will be on the Mini Micro implementation, since that is where the need is most urgent.

Problem: Managing Libraries and Dependencies

Currently if there are libraries you like to use, you have to manually copy them into each project. This causes friction and is error-prone, especially if you are still working on those libraries, in which case it is far too easy to end up with multiple branches containing different features.

A current work-around is to have a separate library folder and mount that as /usr2, and then include that in env.importPaths. But then when you go to distribute your project, you have to remember to copy the used libraries into your /usr disk, which again is error-prone. And any time you send anyone code, you have to be sure to include the external libraries it uses, or instructions on how the user should get them.

Proposal: MiniScript Package Manager

Let's make a standard package manager (exact name TBD) that makes it easy to define what packages/libraries your project needs, and automatically installs them when you don't already have them. Key features:

  • Requirements are version-locked and hash-locked, protecting your project from breaking due to package updates as well as supply-chain attacks where somebody uploads malware in place of a common package.
  • Required packages are automatically downloaded and installed when the project is run, so the user doesn't have to worry about it; they can just clone/download and run, and it should Just Work.
  • This runtime validation should be quick enough to not significantly impact startup time when the needed packages are already present.
  • The system should be secure, not enabling project or package developers to get any greater access to the host system than ordinary MiniScript code already has.

Storage Locations

Phase 1: Local Only
In the initial phase, downloaded packages are always installed in a packages folder at the root level of the project (typically accessed in Mini Micro as /usr/packages), which is auto-created if it doesn't already exist. Note that project authors would typically .gitignore the packages folder, so that used packages are not copied up to GitHub along with their project. But when making a distribution for download, they might choose to include it, so that the distro can run without an internet connection or to save the user download time.

Phase 2: Global Option
As a later feature, we allow the user to define some other directory on the host system where packages may be stored. In Mini Micro, this would probably require some UI (perhaps in the BIOS?) for the user to see/change this path. The requirement system would look first in local packages, then in global; and when installing a previously un-installed package, would default to installing globally (or perhaps this is an option the user can set through a config file in the global directory).

Syntax & Behavior

The core part of this manager would be a require command, which takes a specification for a package that is needed. It would look like this:

require "github.com/JoeStrout/spriteExtras@v0.1.0", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"

This specifies the library to get via (1) a path to a GitHub (or similar?) project, (2) a release tag, and (3) an archive hash (sha256). This command would appear in some script, before any import that relies on the given package. An alternative would be to give the name of a text file that lists requirements, one per line, e.g.:

[In someScript.ms:]

require "requirements.txt"

[In requirements.txt:]

github.com/JoeStrout/spriteExtras@v0.1.0, e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
github.com/jdoe/doeUtils@v1.0.4, 391567a93439523e8e7b8c38c77554a306ee0513d9dafd84d1a6e190587927f1

In either case, the require command causes the following to happen for each specified package:

  1. The local packages folder is checked for a copy of the given package and release. (With Phase 2, the global packages folder is also checked). If found, the found package is re-hashed unless it has a recent hash value stored with it; and then the hash is compared to the given hash.
  2. If the package was not found, it is downloaded, unpacked, and hashed.
  3. If the hash value does not match, a detailed error is printed, and the program exits.
  4. If the package was found or downloaded and the hash matches, then its directory is inserted at the front of the env.importPaths list (and removed from any later position at which it may be found).
  5. The return value of the require command is that same directory.

Note that no code is actually imported or run from the package at this time; the code will still need to import or otherwise make use of the downloaded files. An import at this point should just work, thanks to the updated env.importPaths. For other usage — for example, loading images or data files included with the package — the code can look in importPaths for the directory, or get it from the return value of the require command.

Note that it is perfectly fine to require the same package more than once in a run. It should execute very quickly on subsequent calls, just returning the directory where the package can be found and moving that directory to the front of the importPaths list.

Indirect Dependencies

The requirements.txt file may have a second section, after a blank line and "Indirect:" header. Requirements after that point are indirect, i.e., not things the current project is using itself, but things that some package its using requires. These are typically found by looking for a requirements.txt file in the packages you require.

The indirect packages are installed when the requirements.txt file is processed, just like the primary requirements. They are separated only to give the package management tool (see below) more context, i.e., there are commands that rewrite the indirect packages by examining the current direct packages.

Package Helper

An interactive program should be provided to help manage the installed packages. This is a later/nice-to-have feature, as it does nothing the user couldn't do manually, but let's spec it out here anyway. It would have the following functions:

  • list: show all installed packages with their versions (and hash?)
  • install: manually install a package without a hash code (reporting the resulting hash)
  • remove: delete a package from the packages folder(s)
  • require: add a package to the requirements.txt file of the current project
  • unrequire: remove a package from requirements.txt
  • find requirements: searches all .ms files in the current project for require commands, and adds them to the primary requirements in requirements.txt; then rewrites the secondary requirements
  • yeet: copy local packages to the global packages folder
  • yoink: copy all required packages into the local packages folder

Security Considerations

  • The hash of each installed package will be recomputed periodically (how often is configurable via a config file in the packages folder, and defaults to 1 day). This protects against something altering an installed package but leaving the old hash value in the cache.
  • When we have a global packages folder, it should be read-only to ordinary MiniScript code. Only the require command and the package helper should be able to write to that directory, and only in very specific ways.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions