{ "cells": [ { "cell_type": "markdown", "id": "086cc19a-639d-441f-8489-9b822e3d5e45", "metadata": { "tags": [] }, "source": [ "# CLI" ] }, { "cell_type": "code", "execution_count": null, "id": "0ff8b805-53e7-40b0-a069-e2b2445ae9ff", "metadata": { "tags": [ "hide-cell" ] }, "outputs": [], "source": [ "import os, pathlib, tempfile, shutil, atexit, hashlib, pprint\n", "from IPython.display import *\n", "from IPython import get_ipython # needed for `jupyter_execute` because magics?\n", "RTD = os.environ.get(\"READTHEDOCS\")" ] }, { "cell_type": "markdown", "id": "7b7c1556-a706-41fc-b858-2d3b55907003", "metadata": { "tags": [ "output_scroll" ] }, "source": [ "The `jupyter lite` (or `jupyter-lite`) CLI provides tools for lifecycle of combining...\n", "- **user-authored content** like _Notebooks_ and _Lab Extensions_.\n", "- the core JupyterLite **static assets** \n", "\n", "... into a **ready-to-[deploy](./deploying.md)** (and optionally **reproducible**) Jupyter sites which require no server." ] }, { "cell_type": "markdown", "id": "716098de-aacc-44ab-a78e-136a1a394bdb", "metadata": {}, "source": [ "## Installation" ] }, { "cell_type": "code", "execution_count": null, "id": "674839c9-ab5c-43d1-9dda-d94554ee790d", "metadata": { "tags": [ "output_scroll" ] }, "outputs": [], "source": [ "!pip install jupyterlite\n", "!jupyter lite --version" ] }, { "cell_type": "markdown", "id": "344d0c39-0f36-493d-ba08-6c8814c05086", "metadata": {}, "source": [ "### Extras" ] }, { "cell_type": "markdown", "id": "2a22683e-3c78-4271-b30a-60de50d2b5af", "metadata": {}, "source": [ "Some extra features of different _addons_ have additional dependencies.\n", "\n", "```bash\n", "pip install jupyterlite[contents] # jupyter_server for better contents API\n", "pip install jupyterlite[serve] # tornado for better local preview\n", "pip install jupyterlite[lab] # a known-compatible jupyterlab (entails `contents` and `serve`)\n", "# TODO: [archive] # use libarchive\n", "```" ] }, { "cell_type": "markdown", "id": "3dce75b4-2588-40d5-a0b3-2335806d7be4", "metadata": {}, "source": [ "## The Lite Dir\n", "\n", "When you run `jupyter lite` commands, it assumes your current working directory is the partial contents of a JupyterLite site. You can override this with `--lite-dir`. By default, the built site will be created in `_output`, but can be overridden with `--output-dir`." ] }, { "cell_type": "code", "execution_count": null, "id": "fefc0961-21dd-42ad-9ec7-56b55b0ec50f", "metadata": { "tags": [ "hide-cell" ] }, "outputs": [], "source": [ "if \"TMP_DIR\" not in globals():\n", " TMP_DIR = pathlib.Path(tempfile.mkdtemp(prefix=\"_my_lite_dir_\"))\n", " def clean():\n", " shutil.rmtree(TMP_DIR)\n", " atexit.register(clean)\n", "os.chdir(TMP_DIR)\n", "print(pathlib.Path.cwd())" ] }, { "cell_type": "markdown", "id": "833eafef-c5fd-4e4e-b3b6-6ed5aed622d6", "metadata": {}, "source": [ "### Well-known Files\n", "\n", "Some files in your `--lite-dir` that have special meaning:" ] }, { "cell_type": "markdown", "id": "c315615d-023c-4d75-b3c8-3a51abc838bf", "metadata": {}, "source": [ "| paths | file | if found |\n", "|-----------------------------|---------------------|------------------------------------------------|\n", "| `.`
`./lab`
`./retro` | `jupyter-lite.{json,ipynb}` | merge contents with static in `_output/{path}/jupyter-lite.{json,ipynb}` |\n", "| `.`
`./lab`
`./retro` | `overrides.json` | merge with static `_output/{*}/jupyter-lite.json` ||\n", "| `./files/` | `*` | copy verbatim to `_output/files/*` and index in `/api/contents` |" ] }, { "cell_type": "markdown", "id": "37bcecbd-af66-4700-a10f-36056d2a10d5", "metadata": {}, "source": [ "## Usage" ] }, { "cell_type": "markdown", "id": "c11085fe-e463-4ef5-b0bf-a468e28f5913", "metadata": {}, "source": [ "### Common Parameters\n", "\n", "| parameter | description | default | environment variable |\n", "| ------------------------ | ----------------------------------------------------------------------------------- | ----------------------------- | --------------------------- |\n", "| `--lite-dir` | configuration and content for the site | current working directory | `JUPYTERLITE_DIR` |\n", "| `--output-dir` | where the hostable site will be created | `_output` | `JUPYTERLITE_OUTPUT_DIR` |\n", "| `--cache-dir` | a cache directory for downloads | `/.cache` | `JUPYTERLITE_CACHE_DIR` |\n", "| `--app-archive` | an alternate site to base off of | bundled | |\n", "| `--files` | directory to copy to `_output/files/` and available as _Contents_ | `./files` | |\n", "| `--ignore-files` | patterns that should _never_ be included in `/files/` (even if found in `lite-dir`) | various | |\n", "| `--output-archive` | the path to the archive | `-jupyterlite.tgz` | `JUPYTERLAB_OUTPUT_ARCHIVE` |\n", "| `--port` | port on `127.0.0.1` to serve the test server | `8000` | `JUPYTERLITE_PORT` |\n", "| `--base-url` | the URL prefix to include before the site | `/` | `JUPYTERLITE_BASE_URL` |\n", "| `--source-date-epoch` | optionally enable additional reproducible build measures (best-effort!) | | `SOURCE_DATE_EPOCH` |\n", "| `--federated-extensions` | paths to folders, `pip`/`conda` packages with extensions [see note](#conda-packages)| | |\n", "| `--ignore-sys-prefix` | don't copy any contents, such as install labextensions, from `sys.prefix` | `False` | |" ] }, { "cell_type": "markdown", "id": "86cf31a3-1dc3-4c7f-a620-3655d02b548a", "metadata": {}, "source": [ "All parameters may be configured via a `jupyter_lite_config.json` in the directory where `jupyter lite` is launched, or given via `--config`. \n", "\n", "```{hint}\n", "For an advanced example, see the [configuration](https://github.com/jupyterlite/jupyterlite/tree/main/examples) used for this documentation.\n", "```" ] }, { "cell_type": "markdown", "id": "e2bf35ba-3be6-4cf0-a0b1-abb00ccbc093", "metadata": {}, "source": [ "### Help\n", "\n", "The CLI provides its own documentation, under `--help` (or `-h`)." ] }, { "cell_type": "code", "execution_count": null, "id": "44bf9602-35ae-4a89-bc01-f8bd65567133", "metadata": { "tags": [ "output_scroll", "hide-output" ] }, "outputs": [], "source": [ "!jupyter lite --help" ] }, { "cell_type": "markdown", "id": "bdfdc1ad-a690-4dfd-b4e2-fe4cfda05443", "metadata": {}, "source": [ "### Status\n", "\n", "Always safe to run, this command provides an overview of what JupyterLite has been doing." ] }, { "cell_type": "code", "execution_count": null, "id": "83cc3606-a0e1-4f88-bd77-12c6d2d74785", "metadata": { "tags": [ "hide-output" ] }, "outputs": [], "source": [ "!jupyter lite status" ] }, { "cell_type": "markdown", "id": "71523b95-096e-4ca9-b383-f453e591df6b", "metadata": {}, "source": [ "### List\n", "\n", "Always safe to run, this command provides an overview of what JupyterLite _might_ do.\n", "\n", "> _TODO: improve on default output_" ] }, { "cell_type": "code", "execution_count": null, "id": "7e2da6c4-d1cc-4a78-a22c-ce88c9d0c46a", "metadata": { "tags": [ "output_scroll", "hide-output" ] }, "outputs": [], "source": [ "!jupyter lite list" ] }, { "cell_type": "markdown", "id": "0fac0352-03c8-4342-aa1e-bbca4d6b9706", "metadata": {}, "source": [ "### Init\n", "\n", "Copy all the static data to the `--output-dir`." ] }, { "cell_type": "code", "execution_count": null, "id": "bff9693b-08a8-4be7-a248-dc3ee8b1055d", "metadata": { "tags": [ "output_scroll", "hide-output" ] }, "outputs": [], "source": [ "!jupyter lite init" ] }, { "cell_type": "markdown", "id": "deb8643d-adb7-42fa-8408-3a6d7ecd2e84", "metadata": {}, "source": [ "### Build\n", "\n", "Copy all the **user-authored content** to the `--output-dir`, and applies appropriate changes to e.g. generated Contents API responses.\n", "\n", "Special well-known files will be _merged_ appropriately, but generally, files that exist in the user directory will overwrite any existing content." ] }, { "cell_type": "code", "execution_count": null, "id": "a234a07b-07d7-4320-b260-f59882d456e6", "metadata": { "tags": [ "output_scroll", "hide-output" ] }, "outputs": [], "source": [ "!jupyter lite build" ] }, { "cell_type": "markdown", "id": "212aa2c4-6a73-494c-a872-9d75693f87e8", "metadata": {}, "source": [ "### Serve\n", "\n", "Serve the `--output-dir` on `http://127.0.0.1:{--port=8000}{--base-url=/}`.\n", "\n", "```{warning}\n", "This is _not_ a production server. Please consider _any_ of the [deployment](./deploying.md) options\n", "before trying to make this something it isn't.\n", "```" ] }, { "cell_type": "code", "execution_count": null, "id": "afb00066-9d95-4e2d-9f43-0a350586e49d", "metadata": { "tags": [ "output_scroll", "hide-output" ] }, "outputs": [], "source": [ "!jupyter lite serve --help" ] }, { "cell_type": "markdown", "id": "6b437ed9-4a8b-42cf-a8da-a1282daed39c", "metadata": {}, "source": [ "### Check\n", "\n", "Use all available mechanisms to verify that the build folder conforms to schema, etc." ] }, { "cell_type": "code", "execution_count": null, "id": "0bf76f11-6a45-4c4f-bc02-3a49f39ab799", "metadata": { "tags": [ "output_scroll", "hide-output" ] }, "outputs": [], "source": [ "!jupyter lite check" ] }, { "cell_type": "markdown", "id": "b554754d-9c38-4f2f-9633-659c83fc8867", "metadata": {}, "source": [ "### Archive\n", "\n", "Turn the _output directory_ into a `.tgz` file. This is usually easier to move around than (sometimes) hundreds of files, and can be used as the baseline for future sites.\n", "\n", "> This command is _relatively_ expensive, and is skipped for documentation purposes" ] }, { "cell_type": "code", "execution_count": null, "id": "05fb9e31-2f5e-497c-9a17-5523da383582", "metadata": { "tags": [ "output_scroll", "hide-output" ] }, "outputs": [], "source": [ "!jupyter lite archive --help\n", "if not RTD:\n", " !jupyter lite archive" ] }, { "cell_type": "markdown", "id": "cc782c85-716b-40b6-b1bd-b4075fe41424", "metadata": {}, "source": [ "But let's talk about a more _reproducible_ asset." ] }, { "cell_type": "code", "execution_count": null, "id": "2f933e5a-a9c5-4f09-824e-b70cc6f2fcdb", "metadata": { "tags": [ "hide-cell" ] }, "outputs": [], "source": [ "# we clean out the TMP_DIR for the reproducibility examples\n", "shutil.rmtree(TMP_DIR / \"_output\", ignore_errors=True)" ] }, { "cell_type": "markdown", "id": "d6376cc0-94bf-4558-8808-824364a03cd7", "metadata": {}, "source": [ "\n", "\n", "#### Reproducible Archives\n", "\n", "> _🛠️ This feature is a **work-in-progress**, and should not be relied upon by any production workflows **Just Yet**._" ] }, { "cell_type": "markdown", "id": "5206b427-96a1-4e11-9d35-636b166a122e", "metadata": {}, "source": [ "If `--source-date-epoch` is given, a number of measures will be taken to *try to ensure* that the output of `jupyter lite archive`, an npm-compatible `tgz` package, always returns a bit-for-bit [reproducible build](https://reproducible-builds.org).\n", "\n", "The most obvious change is that the modified time of each file \"clamped\" to that time. Some other changes:\n", "- file ownership will be reset\n", "- predictable sorting will be used\n", "- additional checks will be applied\n", "\n", "```{note}\n", "This is a shortcut for setting the environment variable `SOURCE_DATE_EPOCH`:\n", "\n", "| platform | command |\n", "|------------------|-------------------------------------------------------|\n", "| Linux
MacOS | `export SOURCE_DATE_EPOCH=` |\n", "| Windows | `set SOURCE_DATE_EPOCH=` |\n", "| Python | `os.environ.update(SOURCE_DATE_EPOCH, )` |\n", "```" ] }, { "cell_type": "code", "execution_count": null, "id": "3c83b952-793a-4def-9266-4371ce8a1134", "metadata": { "tags": [ "hide-input" ] }, "outputs": [], "source": [ "if not \"source_date_epoch\" in globals():\n", " from datetime import datetime\n", " source_date_epoch = int(datetime.utcnow().timestamp())\n", "\n", "print(\"SOURCE_DATE_EPOCH is\", source_date_epoch)" ] }, { "cell_type": "code", "execution_count": null, "id": "692f442b-2def-40b3-a7d4-3503c57b56a5", "metadata": { "tags": [ "output_scroll", "hide-output" ] }, "outputs": [], "source": [ "if not RTD:\n", " !jupyter lite archive --source-date-epoch {source_date_epoch} --output-archive ./a.tgz" ] }, { "cell_type": "markdown", "id": "0071cee2-6ce3-4b63-969b-b35842b30e96", "metadata": {}, "source": [ "If we clear out our `_output`..." ] }, { "cell_type": "code", "execution_count": null, "id": "fdd37b3d-5018-4b38-aa50-d82fcfdc3001", "metadata": { "tags": [ "hide-input" ] }, "outputs": [], "source": [ "if not RTD:\n", " shutil.rmtree(TMP_DIR / \"_output\", ignore_errors=True)\n", " pprint.pprint([*TMP_DIR.rglob(\"*\")])" ] }, { "cell_type": "markdown", "id": "785b23e2-84e6-4dcf-8e22-ef73dae059c1", "metadata": {}, "source": [ "...and rebuild, we should always get the same file." ] }, { "cell_type": "code", "execution_count": null, "id": "1f9027d9-ac28-4e9d-a90a-8db2835809a0", "metadata": { "tags": [ "output_scroll", "hide-output" ] }, "outputs": [], "source": [ "if not RTD:\n", " !jupyter lite archive --source-date-epoch {source_date_epoch} --output-archive ./b.tgz" ] }, { "cell_type": "code", "execution_count": null, "id": "6dc180b1-72ae-489d-9271-49337066062f", "metadata": { "tags": [ "hide-input" ] }, "outputs": [], "source": [ "if not RTD:\n", " a, b = [\n", " hashlib.sha256((TMP_DIR / f\"{x}.tgz\").read_bytes()).hexdigest() \n", " for x in \"ab\"\n", " ]\n", " print(\"We built app archives with the SHA256SUMS of:\\n\", a, \"\\n\", b)\n", " try:\n", " assert a == b, \"We did not reproducibly build today.\\n- {}\\n- {}\\n\\n\".format(a, b)\n", " except AssertionError as err:\n", " if shutil.which(\"diffoscope\"):\n", " print(\"We did NOT reproducibly build today, checking in with `diffoscope`...\")\n", " !diffoscope a.tgz b.tgz\n", " print(\"...but at least we tried REALLY hard!\\n\")" ] }, { "cell_type": "markdown", "id": "a8cc9e98-a8af-41d0-8d3b-08db7e5153be", "metadata": {}, "source": [ "## Miscellaneous" ] }, { "cell_type": "markdown", "id": "9b081fe0-7720-4327-a087-c9555ff14fb9", "metadata": {}, "source": [ "### conda packages\n", "\n", "While `--federated-extensions` support the `.tar.bz2` created by most `conda` packages, there are some issues:\n", "\n", "- `anaconda.org` uses non-standard HTTP headers to S3 buckets to provide packages\n", "- the `conda-forge` channel provides all of its builds as GitHub releases, and can be predictably transformed, e.g.\n", "\n", "```\n", "https://anaconda.org/conda-forge/jupyterlab_widgets/1.0.0/download/noarch/jupyterlab_widgets-1.0.0-pyhd8ed1ab_1.tar.bz2\n", " | |\n", " | +---------------------------------------------+\n", " v v v\n", " https://github.com/conda-forge/releases/releases/download/noarch/jupyterlab_widgets-1.0.0-pyhd8ed1ab_1.tar.bz2/jupyterlab_widgets-1.0.0-pyhd8ed1ab_1.tar.bz2\n", "```" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.10" }, "toc-autonumbering": true, "toc-showtags": false }, "nbformat": 4, "nbformat_minor": 5 }