ENH Add `micropip.freeze`. This adds a function `micropip.freeze()` which creates a `packages.json`
lock file which includes all packages that were loaded by micropip in the
current Pyodide session. This can be used in subsequent sessions to load
the same set of packages with `pyodide.loadPackage`.

For example in our repl:

```py
from js.console import log
import micropip
await micropip.install("sphinx-version-warning") # Installs 19 wheels from pypi
log(micropip.freeze())
```
Then opening the browser console, we can copy the JSON and make a new `packages.json`
file out of it. (Our repl will just say "<Long output truncated>" but the browser
console truncates it and provides tools to make it easy to copy.) Reloading the page
with the new `packages.json`, `versionwarning` will autoload:
```py
import versionwarning # Automatically loads 19 wheels from PyPI
```
We detected that `versionwarning` is an export provided by the `sphinx-version-warning`
package via the `top_level.txt` file. Distressingly this file is part of the `egg-info`
spec but isn't mentioned anywhere in the `dist-info` spec. But wheels seem to include
it nonetheless.
### Description

### Checklists

- [x] Add a [CHANGELOG](https://github.com/pyodide/pyodide/blob/main/docs/project/changelog.md) entry
- [x] Add / update tests
- [x] Add new / update outdated documentation

#!/usr/bin/env bash

PYODIDE_IMAGE_REPO="pyodide"
PYODIDE_IMAGE_TAG="20220504-py310-chrome101-firefox100"
PYODIDE_PREBUILT_IMAGE_TAG="0.20.0"
DEFAULT_PYODIDE_DOCKER_IMAGE="${PYODIDE_IMAGE_REPO}/pyodide-env:${PYODIDE_IMAGE_TAG}"
DEFAULT_PYODIDE_SYSTEM_PORT="none"
DOCKER_COMMAND="/bin/bash"
DOCKER_INTERACTIVE="--interactive"


USER_HOME="/src/.docker_home"
USER_NAME="$(id -u -n)"
USER_ID="$(id -u)"
USER_GID="$(id -g)"
USER_COMMENT_FIELD="${USER_NAME} pyodide user alias"
USER_INTERPRETER="/sbin/nologin"
USER_FLAG=("--user" "$USER_ID:$USER_GID")
HEALTHCHECK_MESSAGE="Done initialization"

set -eo pipefail


function usage() {
  cat > /dev/stdout <<EOF
Usage: run_docker [OPTIONS] [COMMAND] [ARG...]

Runs COMMAND in a new Pyodide docker container. If no COMMAND is provided, starts a bash
shell in the container.

Options:
  -h, --help                  Show this information and exit.
  --pre-built                 Use the prebuilt Pyodide image.
                              This is ignored if the env var PYODIDE_DOCKER_IMAGE is set.
  -p, --port <port>           System port to which to forward.
                              This is ignored if the env var PYODIDE_SYSTEM_PORT is set.
                              If set to 'none', docker instance will not bind to any port.
  --non-interactive           Run docker without the --interactive flag.
                              Useful for running in headless mode on CI server.
  --root                      Run as root user inside the container

Prerequisites:
  Docker has to be set up on your system.
EOF
}

function error() {
  usage
  exit 255
}

while [[ $# -gt 0 ]]
do
  key="$1"
  case $key in
      -h|--help)
        usage
        exit 0
      ;;
      --pre-built)
        if [[ -n ${PYODIDE_DOCKER_IMAGE} ]]; then
          echo "WARNING: will use the env var PYODIDE_DOCKER_IMAGE=${PYODIDE_DOCKER_IMAGE},
          the flag --pre-built has no effect"
        fi
        DEFAULT_PYODIDE_DOCKER_IMAGE="pyodide/pyodide:${PYODIDE_PREBUILT_IMAGE_TAG}"
        shift
      ;;
      -p|--port)
        if [ "$#" -lt 2 ]; then
          >&2 echo "port cannot be empty"
          error
        fi
        if [[ -n ${PYODIDE_SYSTEM_PORT} ]]; then
          echo "WARNING: will use the env var PYODIDE_SYSTEM_PORT=${PYODIDE_SYSTEM_PORT} instead of the provided port"
        else
          PYODIDE_SYSTEM_PORT=$2
        fi
        shift 2
      ;;
      --non-interactive)
        DOCKER_INTERACTIVE="--interactive=false"
        shift
      ;;
      --root)
        USER_FLAG=()
        shift
      ;;
      -*)
        >&2 echo "Unknown option $1"
        error
      ;;
      *)
        DOCKER_COMMAND="$*"
        break
      ;;
  esac
done

PYODIDE_DOCKER_PORT=${PYODIDE_DOCKER_PORT:-"8000"}
PYODIDE_SYSTEM_PORT=${PYODIDE_SYSTEM_PORT:-${DEFAULT_PYODIDE_SYSTEM_PORT}}
PYODIDE_DOCKER_IMAGE=${PYODIDE_DOCKER_IMAGE:-${DEFAULT_PYODIDE_DOCKER_IMAGE}}

# in case the port is not a number, do not bind the port
case $PYODIDE_SYSTEM_PORT in
  none)
  PORT_CONFIGURATION_LINE=()
  ;;
  ''|*[!0-9]*) # contains a non-digit character, therefore it is not a number
  echo "WARNING: Invalid port argument '$PYODIDE_SYSTEM_PORT'. Port binding disabled."
  PORT_CONFIGURATION_LINE=()
  ;;
  *)
  PORT_CONFIGURATION_LINE=("-p" "$PYODIDE_SYSTEM_PORT:$PYODIDE_DOCKER_PORT")
  ;;
esac

mkdir -p .docker_home

# Start a detached container as root, add the host uname and uid to /etc/passwd,
# then run forever
CONTAINER=$(\
  docker run \
    "${PORT_CONFIGURATION_LINE[@]}" \
    -d --rm \
    -v "$PWD":/src \
    --user root \
    --shm-size 2g \
    "${PYODIDE_DOCKER_IMAGE}" \
    /bin/bash -c " \
      groupadd '$USER_GID'; \
      useradd \
        --home-dir '$USER_HOME' \
        --uid '$USER_ID' \
        --gid '$USER_GID' \
        --comment '$USER_COMMENT_FIELD' \
        --shell '$USER_INTERPRETER' \
        --groups sudo  \
        $USER_NAME \
      ; \
      echo '%sudo ALL=(ALL:ALL) NOPASSWD:ALL' >> /etc/sudoers ; \
      echo '$HEALTHCHECK_MESSAGE'; \
      tail -f /dev/null \
    " \
)

until docker logs "$CONTAINER" 2>&1 | grep -q "$HEALTHCHECK_MESSAGE"; do
  echo "Waiting for initialization to finish..."
  sleep 0.2
done

EXIT_STATUS=0
# Execute the provided command as the host user with HOME=/src
docker exec \
  "$DOCKER_INTERACTIVE" --tty \
  "${USER_FLAG[@]}" \
  "$CONTAINER" \
  /bin/bash -c "${DOCKER_COMMAND}" || EXIT_STATUS=$?

docker kill "$CONTAINER" > /dev/null
exit $EXIT_STATUS

#!/usr/bin/env bash

PYODIDE_IMAGE_REPO="pyodide"
PYODIDE_IMAGE_TAG="20220504-py310-chrome101-firefox100"
PYODIDE_PREBUILT_IMAGE_TAG="0.20.0"
DEFAULT_PYODIDE_DOCKER_IMAGE="${PYODIDE_IMAGE_REPO}/pyodide-env:${PYODIDE_IMAGE_TAG}"
DEFAULT_PYODIDE_SYSTEM_PORT="none"
DOCKER_COMMAND="/bin/bash"
DOCKER_INTERACTIVE="--interactive"


USER_HOME="/src/.docker_home"
USER_NAME="$(id -u -n)"
USER_ID="$(id -u)"
USER_GID="$(id -g)"
USER_COMMENT_FIELD="${USER_NAME} pyodide user alias"
USER_INTERPRETER="/sbin/nologin"
USER_FLAG=("--user" "$USER_ID:$USER_GID")
HEALTHCHECK_MESSAGE="Done initialization"

set -eo pipefail


function usage() {
  cat > /dev/stdout <<EOF
Usage: run_docker [OPTIONS] [COMMAND] [ARG...]

Runs COMMAND in a new Pyodide docker container. If no COMMAND is provided, starts a bash
shell in the container.

Options:
  -h, --help                  Show this information and exit.
  --pre-built                 Use the prebuilt Pyodide image.
                              This is ignored if the env var PYODIDE_DOCKER_IMAGE is set.
  -p, --port <port>           System port to which to forward.
                              This is ignored if the env var PYODIDE_SYSTEM_PORT is set.
                              If set to 'none', docker instance will not bind to any port.
  --non-interactive           Run docker without the --interactive flag.
                              Useful for running in headless mode on CI server.
  --root                      Run as root user inside the container

Prerequisites:
  Docker has to be set up on your system.
EOF
}

function error() {
  usage
  exit 255
}

while [[ $# -gt 0 ]]
do
  key="$1"
  case $key in
      -h|--help)
        usage
        exit 0
      ;;
      --pre-built)
        if [[ -n ${PYODIDE_DOCKER_IMAGE} ]]; then
          echo "WARNING: will use the env var PYODIDE_DOCKER_IMAGE=${PYODIDE_DOCKER_IMAGE},
          the flag --pre-built has no effect"
        fi
        DEFAULT_PYODIDE_DOCKER_IMAGE="pyodide/pyodide:${PYODIDE_PREBUILT_IMAGE_TAG}"
        shift
      ;;
      -p|--port)
        if [ "$#" -lt 2 ]; then
          >&2 echo "port cannot be empty"
          error
        fi
        if [[ -n ${PYODIDE_SYSTEM_PORT} ]]; then
          echo "WARNING: will use the env var PYODIDE_SYSTEM_PORT=${PYODIDE_SYSTEM_PORT} instead of the provided port"
        else
          PYODIDE_SYSTEM_PORT=$2
        fi
        shift 2
      ;;
      --non-interactive)
        DOCKER_INTERACTIVE="--interactive=false"
        shift
      ;;
      --root)
        USER_FLAG=()
        shift
      ;;
      -*)
        >&2 echo "Unknown option $1"
        error
      ;;
      *)
        DOCKER_COMMAND="$*"
        break
      ;;
  esac
done

PYODIDE_DOCKER_PORT=${PYODIDE_DOCKER_PORT:-"8000"}
PYODIDE_SYSTEM_PORT=${PYODIDE_SYSTEM_PORT:-${DEFAULT_PYODIDE_SYSTEM_PORT}}
PYODIDE_DOCKER_IMAGE=${PYODIDE_DOCKER_IMAGE:-${DEFAULT_PYODIDE_DOCKER_IMAGE}}

# in case the port is not a number, do not bind the port
case $PYODIDE_SYSTEM_PORT in
  none)
  PORT_CONFIGURATION_LINE=()
  ;;
  ''|*[!0-9]*) # contains a non-digit character, therefore it is not a number
  echo "WARNING: Invalid port argument '$PYODIDE_SYSTEM_PORT'. Port binding disabled."
  PORT_CONFIGURATION_LINE=()
  ;;
  *)
  PORT_CONFIGURATION_LINE=("-p" "$PYODIDE_SYSTEM_PORT:$PYODIDE_DOCKER_PORT")
  ;;
esac

mkdir -p .docker_home

# Start a detached container as root, add the host uname and uid to /etc/passwd,
# then run forever
CONTAINER=$(\
  docker run \
    "${PORT_CONFIGURATION_LINE[@]}" \
    -d --rm \
    -v "$PWD":/src \
    --user root \
    --shm-size 2g \
    "${PYODIDE_DOCKER_IMAGE}" \
    /bin/bash -c " \
      groupadd '$USER_GID'; \
      useradd \
        --home-dir '$USER_HOME' \
        --uid '$USER_ID' \
        --gid '$USER_GID' \
        --comment '$USER_COMMENT_FIELD' \
        --shell '$USER_INTERPRETER' \
        --groups sudo  \
        $USER_NAME \
      ; \
      echo '%sudo ALL=(ALL:ALL) NOPASSWD:ALL' >> /etc/sudoers ; \
      echo '$HEALTHCHECK_MESSAGE'; \
      tail -f /dev/null \
    " \
)

until docker logs "$CONTAINER" 2>&1 | grep -q "$HEALTHCHECK_MESSAGE"; do
  echo "Waiting for initialization to finish..."
  sleep 0.2
done

EXIT_STATUS=0
# Execute the provided command as the host user with HOME=/src
docker exec \
  "$DOCKER_INTERACTIVE" --tty \
  "${USER_FLAG[@]}" \
  "$CONTAINER" \
  /bin/bash -c "${DOCKER_COMMAND}" || EXIT_STATUS=$?

docker kill "$CONTAINER" > /dev/null
exit $EXIT_STATUS
