From aef57ba471f6250a434ca389c1895952dfb84fd8 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Fri, 11 Jun 2021 12:18:51 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=81=20Built-in=20Pkg=20management=20(#?= =?UTF-8?q?844)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/Test.yml | 15 +- Project.toml | 5 +- frontend/components/Cell.js | 2 + frontend/components/CellInput.js | 124 +++- frontend/components/Editor.js | 46 +- frontend/components/Notebook.js | 17 +- frontend/components/PkgPopup.js | 130 ++++ frontend/components/PkgStatusMark.js | 118 ++++ frontend/components/PkgTerminalView.js | 17 + frontend/components/RunArea.js | 5 +- frontend/editor.css | 206 ++++++ frontend/editor.html | 1 + frontend/imports/AnsiUp.js | 3 + sample/JavaScript.jl | 98 ++- sample/Plots.jl.jl | 851 ++++++++++++++++++++++++- sample/PlutoUI.jl.jl | 110 +++- sample/old_notebook_with_using.jl | 28 + src/Configuration.jl | 1 + src/Pluto.jl | 10 + src/analysis/TopologyUpdate.jl | 1 - src/evaluation/Run.jl | 18 +- src/evaluation/Tokens.jl | 2 - src/evaluation/WorkspaceManager.jl | 55 +- src/notebook/Cell.jl | 2 +- src/notebook/Notebook.jl | 107 +++- src/notebook/PathHelpers.jl | 2 + src/packages/Packages.jl | 494 ++++++++++++++ src/packages/PkgCompat.jl | 449 +++++++++++++ src/packages/PkgUtils.jl | 238 +++++++ src/webserver/Dynamic.jl | 42 +- src/webserver/MsgPack.jl | 5 + src/webserver/REPLTools.jl | 30 +- src/webserver/SessionActions.jl | 37 +- src/webserver/Static.jl | 4 +- test/ExpressionExplorer.jl | 1 - test/Notebook.jl | 89 ++- test/WorkspaceManager.jl | 2 +- test/helpers.jl | 8 +- test/packages/Basic.jl | 525 +++++++++++++++ test/packages/PkgCompat.jl | 122 ++++ test/packages/corrupted_manifest.jl | 40 ++ test/packages/old_artifacts_import.jl | 119 ++++ test/packages/old_import.jl | 19 + test/packages/simple_import.jl | 109 ++++ test/packages/unregistered_import.jl | 98 +++ test/runtests.jl | 16 +- 46 files changed, 4237 insertions(+), 184 deletions(-) create mode 100644 frontend/components/PkgPopup.js create mode 100644 frontend/components/PkgStatusMark.js create mode 100644 frontend/components/PkgTerminalView.js create mode 100644 frontend/imports/AnsiUp.js create mode 100644 sample/old_notebook_with_using.jl create mode 100644 src/packages/Packages.jl create mode 100644 src/packages/PkgCompat.jl create mode 100644 src/packages/PkgUtils.jl create mode 100644 test/packages/Basic.jl create mode 100644 test/packages/PkgCompat.jl create mode 100644 test/packages/corrupted_manifest.jl create mode 100644 test/packages/old_artifacts_import.jl create mode 100644 test/packages/old_import.jl create mode 100644 test/packages/simple_import.jl create mode 100644 test/packages/unregistered_import.jl diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 5a7b4f2bde..c07afbcd5f 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -25,18 +25,8 @@ jobs: strategy: matrix: # We test quite a lot of versions because we do some OS and version specific things unfortunately - julia-version: ["1.0", "1.1", "1.2", "1.3", "1.4", "1.5", "1.6"] - os: [ubuntu-latest] - include: - # macOS and windows - only the extremes, linux covers pretty much everything already i think - - julia-version: "1.0" - os: macOS-latest - - julia-version: "^1.6.0-0" - os: macOS-latest - - julia-version: "1.0" - os: windows-latest - - julia-version: "^1.6.0-0" - os: windows-latest + julia-version: ["1.5", "1.6", "nightly"] + os: [ubuntu-latest, macOS-latest, windows-latest] steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it @@ -49,3 +39,4 @@ jobs: # 🚗 - uses: julia-actions/julia-runtest@v1 + continue-on-error: ${{ matrix.julia-version == 'nightly' }} diff --git a/Project.toml b/Project.toml index 29f45da08c..afebff8713 100644 --- a/Project.toml +++ b/Project.toml @@ -2,13 +2,14 @@ name = "Pluto" uuid = "c3e4b0f8-55cb-11ea-2926-15256bba5781" license = "MIT" authors = ["Fons van der Plas "] -version = "0.14.8" +version = "0.15.0" [deps] Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" Configurations = "5218b696-f38b-4ac9-8b61-a12ec717816d" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" +FileWatching = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" FuzzyCompletions = "fb4132e2-a121-4a70-b8a1-d5b831dcdcc2" HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" @@ -29,7 +30,7 @@ HTTP = "^0.9.1" MsgPack = "1.1" TableIOInterface = "0.1" Tables = "1" -julia = "^1.0.4" +julia = "^1.5" [extras] DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" diff --git a/frontend/components/Cell.js b/frontend/components/Cell.js index d119a291d0..fc96120f5b 100644 --- a/frontend/components/Cell.js +++ b/frontend/components/Cell.js @@ -34,6 +34,7 @@ export const Cell = ({ focus_after_creation, is_process_ready, disable_input, + nbpkg, }) => { let pluto_actions = useContext(PlutoContext) const notebook = pluto_actions.get_notebook() @@ -180,6 +181,7 @@ export const Cell = ({ }} on_update_doc_query=${on_update_doc_query} on_focus_neighbor=${on_focus_neighbor} + nbpkg=${nbpkg} cell_id=${cell_id} notebook_id=${notebook_id} running_disabled=${running_disabled} diff --git a/frontend/components/CellInput.js b/frontend/components/CellInput.js index b47f98162b..64ae0b3b32 100644 --- a/frontend/components/CellInput.js +++ b/frontend/components/CellInput.js @@ -4,6 +4,7 @@ import observablehq_for_myself from "../common/SetupCellEnvironment.js" import { utf8index_to_ut16index } from "../common/UnicodeTools.js" import { has_ctrl_or_cmd_pressed, map_cmd_to_ctrl_on_mac } from "../common/KeyboardShortcuts.js" import { PlutoContext } from "../common/PlutoContext.js" +import { nbpkg_fingerprint, PkgStatusMark } from "./PkgStatusMark.js" //@ts-ignore import { mac, chromeOS } from "https://cdn.jsdelivr.net/gh/codemirror/CodeMirror@5.60.0/src/util/browser.js" @@ -19,6 +20,24 @@ const clear_selection = (cm) => { const last = (x) => x[x.length - 1] const all_equal = (x) => x.every((y) => y === x[0]) +const swap = (a, i, j) => { + ;[a[i], a[j]] = [a[j], a[i]] +} +const range = (a, b) => { + const x = Math.min(a, b) + const y = Math.max(a, b) + return [...Array(y + 1 - x).keys()].map((i) => i + x) +} + +const get = (map, key, creator) => { + if (map.has(key)) { + return map.get(key) + } else { + const val = creator() + map.set(key, val) + return val + } +} // Adapted from https://gomakethings.com/how-to-test-if-an-element-is-in-the-viewport-with-vanilla-javascript/ var offsetFromViewport = function (elem) { @@ -56,6 +75,7 @@ export const CellInput = ({ on_update_doc_query, on_focus_neighbor, on_drag_drop_events, + nbpkg, cell_id, notebook_id, running_disabled, @@ -73,6 +93,74 @@ export const CellInput = ({ const time_last_being_force_focussed_ref = useRef(0) const time_last_genuine_backspace = useRef(0) + const pkg_bubbles = useRef(new Map()) + + const nbpkg_ref = useRef(nbpkg) + useEffect(() => { + nbpkg_ref.current = nbpkg + pkg_bubbles.current.forEach((b) => { + b.on_nbpkg(nbpkg) + }) + // console.log("nbpkg effect!", nbpkg_fingerprint(nbpkg)) + }, nbpkg_fingerprint(nbpkg)) + + const update_line_bubbles = (line_i) => { + const cm = cm_ref.current + /** @type {string} */ + const line = cm.getLine(line_i) + if (line != undefined) { + // search for the "import Example, Plots" expression using regex + + // dunno + // const re = /(using|import)\s*(\w+(?:\,\s*\w+)*)/g + + // import A: b. c + // const re = /(using|import)(\s*\w+(\.\w+)*(\s*\:(\s*\w+\,)*(\s*\w+)?))/g + + // import A, B, C + const re = /(using|import)(\s*\w+(\.\w+)*)(\s*\,\s*\w+(\.\w+)*)*/g + // const re = /(using|import)\s*(\w+)/g + for (const import_match of line.matchAll(re)) { + const start = import_match.index + import_match[1].length + + // ask codemirror what its parser found for the "import" or "using" word. If it is not a "keyword", then this is part of a comment or a string. + const import_token = cm.getTokenAt({ line: line_i, ch: start }, true) + + if (import_token.type === "keyword") { + const inner = import_match[0].substr(import_match[1].length) + + // find the package name, e.g. `Plot` for `Plot.Extras.coolplot` + const inner_re = /(\w+)(\.\w+)*/g + for (const package_match of inner.matchAll(inner_re)) { + const package_name = package_match[1] + + if (package_name !== "Base" && package_name !== "Core") { + // if the widget already exists, keep it, if not, create a new one + const widget = get(pkg_bubbles.current, package_name, () => { + const b = PkgStatusMark({ + pluto_actions: pluto_actions, + package_name: package_name, + refresh_cm: () => cm.refresh(), + notebook_id: notebook_id, + }) + b.on_nbpkg(nbpkg_ref.current) + return b + }) + + cm.setBookmark( + { line: line_i, ch: start + package_match.index + package_match[0].length }, + { + widget: widget, + } + ) + } + } + } + } + } + } + const update_all_line_bubbles = () => range(0, cm_ref.current.lineCount() - 1).forEach(update_line_bubbles) + useEffect(() => { const first_time = remote_code_ref.current == null const current_value = cm_ref.current?.getValue() ?? "" @@ -86,6 +174,7 @@ export const CellInput = ({ cm_ref.current?.setValue(remote_code) if (first_time) { cm_ref.current.clearHistory() + update_all_line_bubbles() } } }, [remote_code]) @@ -130,6 +219,8 @@ export const CellInput = ({ }, })) + setTimeout(update_all_line_bubbles, 300) + const keys = {} keys["Shift-Enter"] = () => on_submit() @@ -219,14 +310,7 @@ export const CellInput = ({ cm.setSelections(new_selections) } } - const swap = (a, i, j) => { - ;[a[i], a[j]] = [a[j], a[i]] - } - const range = (a, b) => { - const x = Math.min(a, b) - const y = Math.max(a, b) - return [...Array(y + 1 - x).keys()].map((i) => i + x) - } + const alt_move = (delta) => { const selections = cm.listSelections() const selected_lines = new Set([].concat(...selections.map((sel) => range(sel.anchor.line, sel.head.line)))) @@ -454,12 +538,31 @@ export const CellInput = ({ }, 0) }) - cm.on("change", (_, e) => { + cm.on("change", (cm, e) => { + // console.log("cm changed event ", e) const new_value = cm.getValue() if (new_value.length > 1 && new_value[0] === "?") { window.dispatchEvent(new CustomEvent("open_live_docs")) } on_change_ref.current(new_value) + + // remove the currently attached widgets from the codemirror DOM. Widgets corresponding to package imports that did not changed will be re-attached later. + cm.getAllMarks().forEach((m) => { + const m_position = m.find() + if (e.from.line <= m_position.line && m_position.line <= e.to.line) { + m.clear() + } + }) + + // TODO: split this function into a search that returns the list of mathces and an updater + // we can use that when you submit the cell to definitively find the list of import + // and then purge the map? + + // TODO: debounce _any_ edit to update all imports for this cell + // because adding #= to the start of a cell will remove imports later + + // iterate through changed lines + range(e.from.line, e.to.line).forEach(update_line_bubbles) }) cm.on("blur", () => { @@ -514,6 +617,9 @@ export const CellInput = ({ document.fonts.ready.then(() => { cm.refresh() }) + + // we initialize with "" and then call setValue to trigger the "change" event + cm.setValue(local_code) }, []) // useEffect(() => { diff --git a/frontend/components/Editor.js b/frontend/components/Editor.js index 6cf8228e56..dedbfb004b 100644 --- a/frontend/components/Editor.js +++ b/frontend/components/Editor.js @@ -16,6 +16,7 @@ import { UndoDelete } from "./UndoDelete.js" import { SlideControls } from "./SlideControls.js" import { Scroller } from "./Scroller.js" import { ExportBanner } from "./ExportBanner.js" +import { PkgPopup } from "./PkgPopup.js" import { slice_utf8, length_utf8 } from "../common/UnicodeTools.js" import { has_ctrl_or_cmd_pressed, ctrl_or_cmd_name, is_mac_keyboard, in_textarea_or_input } from "../common/KeyboardShortcuts.js" @@ -23,6 +24,7 @@ import { handle_log } from "../common/Logging.js" import { PlutoContext, PlutoBondsContext, PlutoJSInitializingContext } from "../common/PlutoContext.js" import { unpack } from "../common/MsgPack.js" import { useDropHandler } from "./useDropHandler.js" +import { PkgTerminalView } from "./PkgTerminalView.js" import { start_binder, BinderPhase } from "../common/Binder.js" import { read_Uint8Array_with_progress, FetchProgress } from "./FetchProgress.js" import { BinderButton } from "./BinderButton.js" @@ -73,6 +75,9 @@ const statusmap = (state) => ({ loading: (BinderPhase.wait_for_user < state.binder_phase && state.binder_phase < BinderPhase.ready) || state.initializing || state.moving_file, process_restarting: state.notebook.process_status === ProcessStatus.waiting_to_restart, process_dead: state.notebook.process_status === ProcessStatus.no_process || state.notebook.process_status === ProcessStatus.waiting_to_restart, + nbpkg_restart_required: state.notebook.nbpkg?.restart_required_msg != null, + nbpkg_restart_recommended: state.notebook.nbpkg?.restart_recommended_msg != null, + nbpkg_disabled: state.notebook.nbpkg?.enabled === false, static_preview: state.static_preview, binder: state.offer_binder || state.binder_phase != null, code_differs: state.notebook.cell_order.some( @@ -146,6 +151,7 @@ const first_true_key = (obj) => { * cell_order: Array, * cell_execution_order: Array, * bonds: { [name: string]: any }, + * nbpkg: Object, * }} */ @@ -168,6 +174,7 @@ const initial_notebook = () => ({ cell_order: [], cell_execution_order: [], bonds: {}, + nbpkg: null, }) export class Editor extends Component { @@ -517,6 +524,10 @@ export class Editor extends Component { true ) }, + get_avaible_versions: async ({ package_name, notebook_id }) => { + const { message } = await this.client.send("nbpkg_available_versions", { package_name: package_name }, { notebook_id: notebook_id }) + return message.versions + }, } const apply_notebook_patches = (patches, old_state = undefined) => @@ -1020,6 +1031,19 @@ patch: ${JSON.stringify( const status = this.cached_status ?? statusmap(this.state) const statusval = first_true_key(status) + const restart_button = (text) => html` { + this.client.send( + "restart_process", + {}, + { + notebook_id: notebook.notebook_id, + } + ) + }} + >${text}` const export_url = (u) => this.state.binder_session_url == null ? `./${u}?id=${this.state.notebook.notebook_id}` @@ -1082,23 +1106,14 @@ patch: ${JSON.stringify( ? "Reconnecting..." : statusval === "loading" ? "Loading..." + : statusval === "nbpkg_restart_required" + ? html`${restart_button("Restart notebook")}${" (required)"}` + : statusval === "nbpkg_restart_recommended" + ? html`${restart_button("Restart notebook")}${" (recommended)"}` : statusval === "process_restarting" ? "Process exited — restarting..." : statusval === "process_dead" - ? html`${"Process exited — "} - { - this.client.send( - "restart_process", - {}, - { - notebook_id: notebook.notebook_id, - } - ) - }} - >restart` + ? html`${"Process exited — "}${restart_button("restart")}` : null } @@ -1127,7 +1142,7 @@ patch: ${JSON.stringify( this.state.notebook.process_status === ProcessStatus.starting || this.state.notebook.process_status === ProcessStatus.ready } /> - <${DropRuler} + <${DropRuler} actions=${this.actions} selected_cells=${this.state.selected_cells} set_scroller=${(enabled) => { @@ -1163,6 +1178,7 @@ patch: ${JSON.stringify( on_update_doc_query=${this.actions.set_doc_query} notebook=${this.state.notebook} /> + <${PkgPopup} notebook=${this.state.notebook}/> <${UndoDelete} recently_deleted=${this.state.recently_deleted} on_click=${() => { diff --git a/frontend/components/Notebook.js b/frontend/components/Notebook.js index 666a0161c3..abb8cabce8 100644 --- a/frontend/components/Notebook.js +++ b/frontend/components/Notebook.js @@ -2,6 +2,7 @@ import { PlutoContext } from "../common/PlutoContext.js" import { html, useContext, useEffect, useMemo, useState } from "../imports/Preact.js" import { Cell } from "./Cell.js" +import { nbpkg_fingerprint } from "./PkgStatusMark.js" let CellMemo = ({ cell_result, @@ -18,6 +19,7 @@ let CellMemo = ({ force_hide_input, is_process_ready, disable_input, + nbpkg, }) => { const selected_cells_diffable_primitive = (selected_cells || []).join("") const { body, last_run_timestamp, mime, persist_js_state, rootassignee } = cell_result?.output || {} @@ -40,6 +42,7 @@ let CellMemo = ({ focus_after_creation=${focus_after_creation} is_process_ready=${is_process_ready} disable_input=${disable_input} + nbpkg=${nbpkg} /> ` }, [ @@ -69,6 +72,7 @@ let CellMemo = ({ focus_after_creation, is_process_ready, disable_input, + ...nbpkg_fingerprint(nbpkg), ]) } @@ -168,6 +172,7 @@ export const Notebook = ({ force_hide_input=${cell_inputs_delayed && i > render_cell_inputs_minimum} is_process_ready=${is_process_ready} disable_input=${disable_input} + nbpkg=${notebook.nbpkg} />` )} @@ -199,7 +204,17 @@ export const NotebookMemo = ({ selected_cells=${selected_cells} /> ` - }, [is_initializing, notebook, cell_inputs_local, on_update_doc_query, on_cell_input, on_focus_neighbor, disable_input, last_created_cell, selected_cells]) + }, [ + is_initializing, + notebook, + cell_inputs_local, + on_update_doc_query, + on_cell_input, + on_focus_neighbor, + disable_input, + last_created_cell, + selected_cells, + ]) } */ export const NotebookMemo = Notebook diff --git a/frontend/components/PkgPopup.js b/frontend/components/PkgPopup.js new file mode 100644 index 0000000000..39c90350ef --- /dev/null +++ b/frontend/components/PkgPopup.js @@ -0,0 +1,130 @@ +import { html, useState, useRef, useLayoutEffect, useEffect, useMemo, useContext } from "../imports/Preact.js" +import immer from "../imports/immer.js" +import observablehq from "../common/SetupCellEnvironment.js" +import { cl } from "../common/ClassTable.js" + +import { RawHTMLContainer, highlight } from "./CellOutput.js" +import { PlutoContext } from "../common/PlutoContext.js" +import { package_status, nbpkg_fingerprint_without_terminal } from "./PkgStatusMark.js" +import { PkgTerminalView } from "./PkgTerminalView.js" +import { useDebouncedTruth } from "./RunArea.js" + +export const PkgPopup = ({ notebook }) => { + let pluto_actions = useContext(PlutoContext) + + const [recent_event, set_recent_event] = useState(null) + const [pkg_status, set_pkg_status] = useState(null) + const pos_ref = useRef("") + + const open = (e) => { + const el = e.detail.status_mark_element + + pos_ref.current = `top: ${el.getBoundingClientRect().top - document.body.getBoundingClientRect().top}px; left: min(max(0px,100vw - 251px - 30px), ${ + el.getBoundingClientRect().left - document.body.getBoundingClientRect().left + }px);` + set_recent_event(e.detail) + } + + useEffect(() => { + const onpointerdown = (e) => { + if (e.target?.closest("pkg-popup") == null && e.target?.closest("pkg-status-mark") == null) { + set_recent_event(null) + } + } + const onkeydown = (e) => { + if (e.key === "Escape") { + set_recent_event(null) + } + } + window.addEventListener("open nbpkg popup", open) + window.addEventListener("pointerdown", onpointerdown) + document.addEventListener("keydown", onkeydown) + + return () => { + window.removeEventListener("open nbpkg popup", open) + window.removeEventListener("pointerdown", onpointerdown) + document.removeEventListener("keydown", onkeydown) + } + }, []) + + useEffect(() => { + let still_valid = true + if (recent_event == null) { + set_pkg_status(null) + } else { + ;(pluto_actions.get_avaible_versions({ package_name: recent_event.package_name, notebook_id: notebook.notebook_id }) ?? Promise.resolve([])).then( + (versions) => { + if (still_valid) { + set_pkg_status(package_status({ nbpkg: notebook.nbpkg, package_name: recent_event.package_name, available_versions: versions })) + } + } + ) + } + return () => { + still_valid = false + } + }, [recent_event, ...nbpkg_fingerprint_without_terminal(notebook.nbpkg)]) + + const [showterminal, set_showterminal] = useState(false) + + const busy = recent_event != null && ((notebook.nbpkg?.busy_packages ?? []).includes(recent_event.package_name) || (notebook.nbpkg?.instantiating ?? false)) + + const debounced_busy = useDebouncedTruth(busy, 2) + useEffect(() => { + set_showterminal(debounced_busy) + }, [debounced_busy]) + + const terminal_value = notebook.nbpkg?.terminal_outputs == null ? null : notebook.nbpkg?.terminal_outputs[recent_event?.package_name] + + const showupdate = pkg_status?.offer_update ?? false + + //
${recent_event?.package_name}
+ return html` + ${pkg_status?.hint ?? "Loading..."} + + <${PkgTerminalView} value=${terminal_value ?? "Loading..."} /> + ` +} diff --git a/frontend/components/PkgStatusMark.js b/frontend/components/PkgStatusMark.js new file mode 100644 index 0000000000..d9d57054e8 --- /dev/null +++ b/frontend/components/PkgStatusMark.js @@ -0,0 +1,118 @@ +import _ from "../imports/lodash.js" +import { html as phtml } from "../imports/Preact.js" + +import observablehq_for_myself from "../common/SetupCellEnvironment.js" +// widgets inside codemirror need to be DOM elements, not Preact VDOM components. So in this code, we will use html from observablehq, which is just like html from Preact, except it creates DOM nodes directly, not Preact VDOM elements. +const html = observablehq_for_myself.html + +export const nbpkg_fingerprint = (nbpkg) => (nbpkg == null ? [null] : Object.entries(nbpkg)) + +export const nbpkg_fingerprint_without_terminal = (nbpkg) => + nbpkg == null ? [null] : Object.entries(nbpkg).flatMap(([k, v]) => (k === "terminal_outputs" ? [] : [v])) + +const can_update = (installed, available) => { + if (installed === "stdlib" || !_.isArray(available)) { + return false + } else { + // return true + return _.last(available) !== installed + } +} + +export const package_status = ({ nbpkg, package_name, available_versions }) => { + let status = null + let hint_raw = null + let hint = null + let offer_update = false + const chosen_version = nbpkg?.installed_versions[package_name] + const busy = (nbpkg?.busy_packages ?? []).includes(package_name) || nbpkg?.instantiating + + if (chosen_version != null || _.isEqual(available_versions, ["stdlib"])) { + if (chosen_version == null || chosen_version === "stdlib") { + status = "installed" + hint_raw = `${package_name} is part of Julia's pre-installed 'standard library'.` + hint = phtml`${package_name} is part of Julia's pre-installed standard library.` + } else { + if (busy) { + status = "busy" + hint_raw = `${package_name} (v${chosen_version}) is installing...` + hint = phtml`
${package_name} v${chosen_version}
is installing...` + } else { + status = "installed" + hint_raw = `${package_name} (v${chosen_version}) is installed in the notebook.` + hint = phtml`
${package_name} v${chosen_version}
is installed in the notebook.` + offer_update = can_update(chosen_version, available_versions) + } + } + } else { + if (_.isArray(available_versions)) { + if (available_versions.length === 0) { + status = "not_found" + hint_raw = `The package "${package_name}" could not be found in the registry. Did you make a typo?` + hint = phtml`The package "${package_name}" could not be found in the registry.
Did you make a typo?
` + } else { + status = "will_be_installed" + hint_raw = `${package_name} (v${_.last(available_versions)}) will be installed in the notebook when you run this cell.` + hint = phtml`
${package_name} v${_.last( + available_versions + )}
will be installed in the notebook when you run this cell.` + } + } + } + + return { status, hint, hint_raw, available_versions, chosen_version, busy, offer_update } +} + +// not preact because we're too cool +export const PkgStatusMark = ({ package_name, refresh_cm, pluto_actions, notebook_id }) => { + const button = html`` + const node = html`${button}` + const nbpkg_ref = { current: null } + const available_versions_ref = { current: null } + + const render = () => { + const { status, hint_raw } = package_status({ + nbpkg: nbpkg_ref.current, + package_name: package_name, + available_versions: available_versions_ref.current, + }) + + node.title = hint_raw + + node.classList.toggle("busy", status === "busy") + node.classList.toggle("installed", status === "installed") + node.classList.toggle("not_found", status === "not_found") + node.classList.toggle("will_be_installed", status === "will_be_installed") + + // We don't need to refresh the codemirror for most updates because every mark is exactly the same size. + // refresh_cm() + } + + node.on_nbpkg = (p) => { + // if nbpkg is switch on/off + if ((nbpkg_ref.current == null) !== (p == null)) { + // refresh codemirror because the mark will appear/disappear + refresh_cm() + setTimeout(refresh_cm, 1000) + } + nbpkg_ref.current = p + render() + } + ;(pluto_actions.get_avaible_versions({ package_name, notebook_id }) ?? Promise.resolve([])).then((versions) => { + available_versions_ref.current = versions + render() + }) + + button.onclick = () => { + window.dispatchEvent( + new CustomEvent("open nbpkg popup", { + detail: { + status_mark_element: node, + package_name: package_name, + }, + }) + ) + } + + return node +} diff --git a/frontend/components/PkgTerminalView.js b/frontend/components/PkgTerminalView.js new file mode 100644 index 0000000000..45a11c109a --- /dev/null +++ b/frontend/components/PkgTerminalView.js @@ -0,0 +1,17 @@ +import AnsiUp from "../imports/AnsiUp.js" +import { html, Component, useState, useEffect, useRef, useLayoutEffect } from "../imports/Preact.js" + +const TerminalViewAnsiUp = ({ value }) => { + const node_ref = useRef(null) + + useEffect(() => { + node_ref.current.innerHTML = AnsiUp.ansi_to_html(value) + node_ref.current.parentElement.scrollTop = 1e5 + }, [value]) + + return html`
` +} + +export const PkgTerminalView = TerminalViewAnsiUp diff --git a/frontend/components/RunArea.js b/frontend/components/RunArea.js index 61a2bc357d..7fa29f57e6 100644 --- a/frontend/components/RunArea.js +++ b/frontend/components/RunArea.js @@ -98,10 +98,9 @@ export const useMillisSinceTruthy = (truthy) => { return truthy ? now - startRunning : undefined } -const NSeconds = 5 -export const useDebouncedTruth = (truthy) => { +export const useDebouncedTruth = (truthy, delay = 5) => { const [mytruth, setMyTruth] = useState(truthy) - const setMyTruthAfterNSeconds = useMemo(() => _.debounce(setMyTruth, NSeconds * 1000), [setMyTruth]) + const setMyTruthAfterNSeconds = useMemo(() => _.debounce(setMyTruth, delay * 1000), [setMyTruth]) useEffect(() => { if (truthy) { setMyTruth(true) diff --git a/frontend/editor.css b/frontend/editor.css index 45529b1c22..332030f823 100644 --- a/frontend/editor.css +++ b/frontend/editor.css @@ -669,6 +669,8 @@ body.binder > header > nav#at_the_top > pluto-filepicker > a { text-decoration: none; } +body.nbpkg_restart_recommended > header, +body.nbpkg_restart_required > header, body.binder.loading > header, body.process_dead > header, body.disconnected > header { @@ -678,6 +680,12 @@ body.disconnected > header { -webkit-backdrop-filter: blur(10px); } +body.nbpkg_restart_recommended > header { + background-color: rgba(114, 192, 255, 0.56); +} +body.nbpkg_restart_required > header { + background-color: rgba(170, 41, 32, 0.56); +} body.process_dead > header { background-color: rgb(230 88 46 / 38%); } @@ -1417,6 +1425,202 @@ pluto-input > button.delete_cell > span.icon::after { background-image: url(https://cdn.jsdelivr.net/gh/ionic-team/ionicons@5.5.1/src/svg/ellipsis-horizontal-circle-outline.svg); } +/* PKG UI */ + +body.nbpkg_disabled pkg-status-mark, +body.nbpkg_disabled pkg-popup { + display: none; +} + +pkg-status-mark { + width: 1em; + height: 1em; + margin: 0px .6em 0px 0.2em; + display: inline-block; +} + +pkg-status-mark > button { + margin: 0px; + padding: 0px; + border: none; + background: none; + cursor: context-menu; + position: relative; + top: -0.2em; + + /* background: rgba(127, 176, 76, 0.24); + border: 3px solid rgba(84, 182, 237, 0); + border-radius: 5px; */ +} + +pkg-status-mark > button > span::after { + display: inline-block; + content: " " !important; + background-size: 1.5em; + height: 1.5em; + width: 1.5em; + + background-image: url(https://cdn.jsdelivr.net/gh/ionic-team/ionicons@5.5.1/src/svg/time-outline.svg); + opacity: 0.3; +} + +pkg-status-mark.installed > button > span::after { + background-image: url(https://cdn.jsdelivr.net/gh/ionic-team/ionicons@5.5.1/src/svg/checkmark-outline.svg); + /* background-image: url(https://cdn.jsdelivr.net/gh/ionic-team/ionicons@5.5.1/src/svg/cloud-done-outline.svg); */ +} +pkg-status-mark.busy > button > span::after { + background-image: url(https://cdn.jsdelivr.net/gh/ionic-team/ionicons@5.5.1/src/svg/sync-outline.svg); + animation: loadspin 3s ease-in-out infinite; +} +pkg-status-mark.not_found > button > span::after { + background-image: url(https://cdn.jsdelivr.net/gh/ionic-team/ionicons@5.5.1/src/svg/cloud-offline-outline.svg); + opacity: 0.6; +} +pkg-status-mark.will_be_installed > button > span::after { + background-image: url(https://cdn.jsdelivr.net/gh/ionic-team/ionicons@5.5.1/src/svg/cloud-download-outline.svg); + opacity: 0.6; +} +pkg-status-mark select { + width: 100%; + white-space: nowrap; + border: none; + background-color: transparent; + font-size: 0.65rem; + font-family: "Space Mono", monospace; + font-weight: 500; + text-align: center; + text-align-last: center; +} + +pkg-status-mark option { + background: #d3752538; + color: #00000060; +} +pkg-status-mark option.installed { + background: unset; + color: black; +} + +pkg-popup { + display: block; + position: absolute; + z-index: 50; + /* left: 1.5em; */ + /* top: calc((1.5em - 200px) * 0.5); */ + width: min(90vw, 251px); + /* min-height: 80px; */ + margin-left: 1.1rem; + margin-top: calc(0.5 * (1rem - 80px)); + background: rgb(255, 255, 255); + border: 3px solid #f0e4ee; + border-radius: 10px; + padding: 8px; + overflow-wrap: break-word; + font-family: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", "Helvetica", "Arial", sans-serif; + opacity: 0; + transform: scale(0.2); + transform-origin: left; + transition: transform 0.5s ease-in-out, opacity 0.1s ease-in-out; + pointer-events: none; +} + +pkg-popup.visible { + opacity: 1; + transform: scale(1); + transition: transform 0.2s ease-in-out, opacity 0.2s ease-in-out; + pointer-events: initial; +} + +pkg-popup.busy { + border: 3px solid hsl(282deg 31% 62%); +} + +pkg-version { + font-family: "Space Mono", monospace; + font-size: 0.75rem; + opacity: 0.5; +} + + +pkg-popup .pkg-buttons { + float: right; + /* position: absolute; */ + /* right: 0px; */ + /* bottom: 0px; */ + display: inline-flex; + height: 1em; + flex-direction: row; +} + +pkg-popup .pkg-buttons > * { + display: block; + height: 17px; + padding: 4px; + box-sizing: content-box; + background: white; + border-radius: 10px; + z-index: 52; + margin-left: -4px; +} + +pkg-popup .toggle-terminal { + right: 20px; +} + +pkg-terminal { + display: block; + /* width: 20rem; */ + cursor: text; + margin-top: 6px; + background: #232433; + color: #ffffff; + border-radius: 6px; + border: 3px solid #c3c3c3; + padding: 3px; +} + +pkg-terminal > .scroller { + max-height: 10rem; + overflow-y: auto; + padding: 4px; + width: 100%; +} + +pkg-terminal pre { + white-space: pre-wrap; + word-break: break-all; + font-size: 0.6rem; + font-family: "Space Mono", monospace; + margin: 0; +} + +pkg-popup pkg-terminal { + display: none; +} + +pkg-popup.showterminal pkg-terminal { + display: block; +} + +@keyframes loadspin { + 0% { + transform: rotate(0deg); + } + 25% { + transform: rotate(180deg); + } + 50% { + transform: rotate(180deg); + } + 75% { + transform: rotate(360deg); + } + 100% { + transform: rotate(360deg); + } +} + + /* RUNAREA */ pluto-runarea { @@ -2009,6 +2213,8 @@ li.CodeMirror-hint.c_Array::before, color: #ef6155; } +li.CodeMirror-hint.c_package::before, +li.CodeMirror-hint.c_Module::before, .cm-s-default span.cm-link { color: #815ba4; } diff --git a/frontend/editor.html b/frontend/editor.html index dd9da91788..822dd8ef4d 100644 --- a/frontend/editor.html +++ b/frontend/editor.html @@ -39,6 +39,7 @@ + diff --git a/frontend/imports/AnsiUp.js b/frontend/imports/AnsiUp.js new file mode 100644 index 0000000000..e7f6abb905 --- /dev/null +++ b/frontend/imports/AnsiUp.js @@ -0,0 +1,3 @@ +// @ts-ignore +export default AnsiUp = new window.AnsiUp() +// export default window.AnsiUp diff --git a/sample/JavaScript.jl b/sample/JavaScript.jl index e9c5524d20..bd1b19265a 100644 --- a/sample/JavaScript.jl +++ b/sample/JavaScript.jl @@ -1,5 +1,5 @@ ### A Pluto.jl notebook ### -# v0.14.7 +# v0.15.0 using Markdown using InteractiveUtils @@ -14,15 +14,7 @@ macro bind(def, element) end # ╔═╡ 571613a1-6b4b-496d-9a68-aac3f6a83a4b -begin - import Pkg - Pkg.activate(mktempdir()) - Pkg.add([ - Pkg.PackageSpec(name="PlutoUI", version="0.7"), - Pkg.PackageSpec(name="HypertextLiteral", version="0.7"), - ]) - using PlutoUI, HypertextLiteral -end +using PlutoUI, HypertextLiteral # ╔═╡ 97914842-76d2-11eb-0c48-a7eedca870fb md""" @@ -997,6 +989,90 @@ $(embed_display(rand(4))) You can [learn more](https://github.com/fonsp/Pluto.jl/pull/1126) about how this feature works, or how to use it inside packages. """ +# ╔═╡ 00000000-0000-0000-0000-000000000001 +PLUTO_PROJECT_TOML_CONTENTS = """ +[deps] +HypertextLiteral = "ac1192a8-f4b3-4bfe-ba22-af5b92cd3ab2" +PlutoUI = "7f904dfe-b85e-4ff6-b463-dae2292396a8" + +[compat] +HypertextLiteral = "^0.7.0" +PlutoUI = "^0.7.9" +""" + +# ╔═╡ 00000000-0000-0000-0000-000000000002 +PLUTO_MANIFEST_TOML_CONTENTS = """ +# This file is machine-generated - editing it directly is not advised + +[[Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" + +[[Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" + +[[HypertextLiteral]] +git-tree-sha1 = "3cd97ad41c50d81e698ec625d52753556cab994e" +uuid = "ac1192a8-f4b3-4bfe-ba22-af5b92cd3ab2" +version = "0.7.0" + +[[InteractiveUtils]] +deps = ["Markdown"] +uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" + +[[JSON]] +deps = ["Dates", "Mmap", "Parsers", "Unicode"] +git-tree-sha1 = "81690084b6198a2e1da36fcfda16eeca9f9f24e4" +uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +version = "0.21.1" + +[[Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" + +[[Markdown]] +deps = ["Base64"] +uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" + +[[Mmap]] +uuid = "a63ad114-7e13-5084-954f-fe012c677804" + +[[Parsers]] +deps = ["Dates"] +git-tree-sha1 = "c8abc88faa3f7a3950832ac5d6e690881590d6dc" +uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" +version = "1.1.0" + +[[PlutoUI]] +deps = ["Base64", "Dates", "InteractiveUtils", "JSON", "Logging", "Markdown", "Random", "Reexport", "Suppressor"] +git-tree-sha1 = "44e225d5837e2a2345e69a1d1e01ac2443ff9fcb" +uuid = "7f904dfe-b85e-4ff6-b463-dae2292396a8" +version = "0.7.9" + +[[Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[[Random]] +deps = ["Serialization"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" + +[[Reexport]] +git-tree-sha1 = "5f6c21241f0f655da3952fd60aa18477cf96c220" +uuid = "189a3867-3050-52da-a836-e630ba90ab69" +version = "1.1.0" + +[[Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" + +[[Suppressor]] +git-tree-sha1 = "a819d77f31f83e5792a76081eee1ea6342ab8787" +uuid = "fd094767-a336-5f1f-9728-57cf17d0bbfb" +version = "0.2.0" + +[[Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" +""" + # ╔═╡ Cell order: # ╟─97914842-76d2-11eb-0c48-a7eedca870fb # ╠═571613a1-6b4b-496d-9a68-aac3f6a83a4b @@ -1073,3 +1149,5 @@ You can [learn more](https://github.com/fonsp/Pluto.jl/pull/1126) about how this # ╠═64cbf19c-a4e3-4cdb-b4ec-1fbe24be55ad # ╟─cc318a19-316f-4fd9-8436-fb1d42f888a3 # ╟─7aacdd8c-1571-4461-ba6e-0fd65dd8d788 +# ╟─00000000-0000-0000-0000-000000000001 +# ╟─00000000-0000-0000-0000-000000000002 diff --git a/sample/Plots.jl.jl b/sample/Plots.jl.jl index 68aaf2d25e..6f43b7b75c 100644 --- a/sample/Plots.jl.jl +++ b/sample/Plots.jl.jl @@ -1,9 +1,12 @@ ### A Pluto.jl notebook ### -# v0.11.12 +# v0.15.0 using Markdown using InteractiveUtils +# ╔═╡ 5e611449-c239-4547-9d13-5db33a081e58 +using Plots + # ╔═╡ 7b93882c-9ad8-11ea-0288-0941e163f9d5 md""" # Plotting in Pluto @@ -12,33 +15,9 @@ Pluto is an excellent enviroment to visualise your data. This notebook shows a f I wrote this notebook so you can understand it even if you have never used `Plots`. However, it is not intend as a complete tutorial for the package. If you want to start making your own plots, I recommend looking at the package [documentation](https://docs.juliaplots.org/latest/) for a full tutorial as well. -Let's start by importing the `Plots` package. We do this in a special way, which guarantees that anyone who opens the notebook can use the package. +Let's start by importing the `Plots` package. _(Pluto's package manager automatically takes care of installing and tracking dependencies!)_ """ -# ╔═╡ d889e13e-f0ff-11ea-3306-57491904d83f -md"_First, we set up a clean package environment:_" - -# ╔═╡ aefb6004-f0ff-11ea-10c9-c504c7aa9fe5 -begin - import Pkg - Pkg.activate(mktempdir()) -end - -# ╔═╡ a0f0ae42-9ad8-11ea-2475-a735df64aa28 -begin - Pkg.add("Plots") - using Plots -end - -# ╔═╡ e48a0e78-f0ff-11ea-2852-2f4eee1a7d2d -md"_Next, we add the `Plots` package to our environment, and we import it._" - -# ╔═╡ a9a3c640-f100-11ea-2e86-5f0e81a37ebd -md"You can just put a cell like this anywhere in your notebook, if you want to add+import more packages." - -# ╔═╡ c042d3d2-f100-11ea-3833-af1f19afd400 -md"(If you already had Plots installed, then this will just use the globally installed version! Neat)" - # ╔═╡ e5abaf40-f105-11ea-1494-2da858d7db5b md"We need to choose a _backend_ for Plots.jl. We choose `plotly` because it works with no additional dependencies. You can [read more about backends](http://docs.juliaplots.org/latest/backends/#backends) in Plots.jl - it's one of its coolest features!" @@ -230,14 +209,820 @@ md""" Try changing the function for the base plot, or the addition in the bottom cell. No weird effects! """ +# ╔═╡ 00000000-0000-0000-0000-000000000001 +PLUTO_PROJECT_TOML_CONTENTS = """ +[compat] +Plots = "~1.16.4" + +[deps] +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +""" + +# ╔═╡ 00000000-0000-0000-0000-000000000002 +PLUTO_MANIFEST_TOML_CONTENTS = """ +# This file is machine-generated - editing it directly is not advised + +[[Adapt]] +deps = ["LinearAlgebra"] +git-tree-sha1 = "84918055d15b3114ede17ac6a7182f68870c16f7" +uuid = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" +version = "3.3.1" + +[[ArgTools]] +uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" + +[[Artifacts]] +uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" + +[[Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" + +[[Bzip2_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "c3598e525718abcc440f69cc6d5f60dda0a1b61e" +uuid = "6e34b625-4abd-537c-b88f-471c36dfa7a0" +version = "1.0.6+5" + +[[Cairo_jll]] +deps = ["Artifacts", "Bzip2_jll", "Fontconfig_jll", "FreeType2_jll", "Glib_jll", "JLLWrappers", "LZO_jll", "Libdl", "Pixman_jll", "Pkg", "Xorg_libXext_jll", "Xorg_libXrender_jll", "Zlib_jll", "libpng_jll"] +git-tree-sha1 = "e2f47f6d8337369411569fd45ae5753ca10394c6" +uuid = "83423d85-b0ee-5818-9007-b63ccbeb887a" +version = "1.16.0+6" + +[[ColorSchemes]] +deps = ["ColorTypes", "Colors", "FixedPointNumbers", "Random", "StaticArrays"] +git-tree-sha1 = "c8fd01e4b736013bc61b704871d20503b33ea402" +uuid = "35d6a980-a343-548e-a6ea-1d62b119f2f4" +version = "3.12.1" + +[[ColorTypes]] +deps = ["FixedPointNumbers", "Random"] +git-tree-sha1 = "024fe24d83e4a5bf5fc80501a314ce0d1aa35597" +uuid = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" +version = "0.11.0" + +[[Colors]] +deps = ["ColorTypes", "FixedPointNumbers", "Reexport"] +git-tree-sha1 = "417b0ed7b8b838aa6ca0a87aadf1bb9eb111ce40" +uuid = "5ae59095-9a9b-59fe-a467-6f913c188581" +version = "0.12.8" + +[[Compat]] +deps = ["Base64", "Dates", "DelimitedFiles", "Distributed", "InteractiveUtils", "LibGit2", "Libdl", "LinearAlgebra", "Markdown", "Mmap", "Pkg", "Printf", "REPL", "Random", "SHA", "Serialization", "SharedArrays", "Sockets", "SparseArrays", "Statistics", "Test", "UUIDs", "Unicode"] +git-tree-sha1 = "e4e2b39db08f967cc1360951f01e8a75ec441cab" +uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" +version = "3.30.0" + +[[CompilerSupportLibraries_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" + +[[Contour]] +deps = ["StaticArrays"] +git-tree-sha1 = "9f02045d934dc030edad45944ea80dbd1f0ebea7" +uuid = "d38c429a-6771-53c6-b99e-75d170b6e991" +version = "0.5.7" + +[[DataAPI]] +git-tree-sha1 = "dfb3b7e89e395be1e25c2ad6d7690dc29cc53b1d" +uuid = "9a962f9c-6df0-11e9-0e5d-c546b8b5ee8a" +version = "1.6.0" + +[[DataStructures]] +deps = ["Compat", "InteractiveUtils", "OrderedCollections"] +git-tree-sha1 = "4437b64df1e0adccc3e5d1adbc3ac741095e4677" +uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" +version = "0.18.9" + +[[DataValueInterfaces]] +git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6" +uuid = "e2d170a0-9d28-54be-80f0-106bbe20a464" +version = "1.0.0" + +[[Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" + +[[DelimitedFiles]] +deps = ["Mmap"] +uuid = "8bb1440f-4735-579b-a4ab-409b98df4dab" + +[[Distributed]] +deps = ["Random", "Serialization", "Sockets"] +uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b" + +[[Downloads]] +deps = ["ArgTools", "LibCURL", "NetworkOptions"] +uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" + +[[EarCut_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "92d8f9f208637e8d2d28c664051a00569c01493d" +uuid = "5ae413db-bbd1-5e63-b57d-d24a61df00f5" +version = "2.1.5+1" + +[[Expat_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "b3bfd02e98aedfa5cf885665493c5598c350cd2f" +uuid = "2e619515-83b5-522b-bb60-26c02a35a201" +version = "2.2.10+0" + +[[FFMPEG]] +deps = ["FFMPEG_jll"] +git-tree-sha1 = "b57e3acbe22f8484b4b5ff66a7499717fe1a9cc8" +uuid = "c87230d0-a227-11e9-1b43-d7ebe4e7570a" +version = "0.4.1" + +[[FFMPEG_jll]] +deps = ["Artifacts", "Bzip2_jll", "FreeType2_jll", "FriBidi_jll", "JLLWrappers", "LAME_jll", "LibVPX_jll", "Libdl", "Ogg_jll", "OpenSSL_jll", "Opus_jll", "Pkg", "Zlib_jll", "libass_jll", "libfdk_aac_jll", "libvorbis_jll", "x264_jll", "x265_jll"] +git-tree-sha1 = "3cc57ad0a213808473eafef4845a74766242e05f" +uuid = "b22a6f82-2f65-5046-a5b2-351ab43fb4e5" +version = "4.3.1+4" + +[[FixedPointNumbers]] +deps = ["Statistics"] +git-tree-sha1 = "335bfdceacc84c5cdf16aadc768aa5ddfc5383cc" +uuid = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" +version = "0.8.4" + +[[Fontconfig_jll]] +deps = ["Artifacts", "Bzip2_jll", "Expat_jll", "FreeType2_jll", "JLLWrappers", "Libdl", "Libuuid_jll", "Pkg", "Zlib_jll"] +git-tree-sha1 = "35895cf184ceaab11fd778b4590144034a167a2f" +uuid = "a3f928ae-7b40-5064-980b-68af3947d34b" +version = "2.13.1+14" + +[[Formatting]] +deps = ["Printf"] +git-tree-sha1 = "8339d61043228fdd3eb658d86c926cb282ae72a8" +uuid = "59287772-0a20-5a39-b81b-1366585eb4c0" +version = "0.4.2" + +[[FreeType2_jll]] +deps = ["Artifacts", "Bzip2_jll", "JLLWrappers", "Libdl", "Pkg", "Zlib_jll"] +git-tree-sha1 = "cbd58c9deb1d304f5a245a0b7eb841a2560cfec6" +uuid = "d7e528f0-a631-5988-bf34-fe36492bcfd7" +version = "2.10.1+5" + +[[FriBidi_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "aa31987c2ba8704e23c6c8ba8a4f769d5d7e4f91" +uuid = "559328eb-81f9-559d-9380-de523a88c83c" +version = "1.0.10+0" + +[[GLFW_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Libglvnd_jll", "Pkg", "Xorg_libXcursor_jll", "Xorg_libXi_jll", "Xorg_libXinerama_jll", "Xorg_libXrandr_jll"] +git-tree-sha1 = "dba1e8614e98949abfa60480b13653813d8f0157" +uuid = "0656b61e-2033-5cc2-a64a-77c0f6c09b89" +version = "3.3.5+0" + +[[GR]] +deps = ["Base64", "DelimitedFiles", "GR_jll", "HTTP", "JSON", "Libdl", "LinearAlgebra", "Pkg", "Printf", "Random", "Serialization", "Sockets", "Test", "UUIDs"] +git-tree-sha1 = "b83e3125048a9c3158cbb7ca423790c7b1b57bea" +uuid = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71" +version = "0.57.5" + +[[GR_jll]] +deps = ["Artifacts", "Bzip2_jll", "Cairo_jll", "FFMPEG_jll", "Fontconfig_jll", "GLFW_jll", "JLLWrappers", "JpegTurbo_jll", "Libdl", "Libtiff_jll", "Pixman_jll", "Pkg", "Qt5Base_jll", "Zlib_jll", "libpng_jll"] +git-tree-sha1 = "e14907859a1d3aee73a019e7b3c98e9e7b8b5b3e" +uuid = "d2c73de3-f751-5644-a686-071e5b155ba9" +version = "0.57.3+0" + +[[GeometryBasics]] +deps = ["EarCut_jll", "IterTools", "LinearAlgebra", "StaticArrays", "StructArrays", "Tables"] +git-tree-sha1 = "4136b8a5668341e58398bb472754bff4ba0456ff" +uuid = "5c1252a2-5f33-56bf-86c9-59e7332b4326" +version = "0.3.12" + +[[Gettext_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl", "Libiconv_jll", "Pkg", "XML2_jll"] +git-tree-sha1 = "9b02998aba7bf074d14de89f9d37ca24a1a0b046" +uuid = "78b55507-aeef-58d4-861c-77aaff3498b1" +version = "0.21.0+0" + +[[Glib_jll]] +deps = ["Artifacts", "Gettext_jll", "JLLWrappers", "Libdl", "Libffi_jll", "Libiconv_jll", "Libmount_jll", "PCRE_jll", "Pkg", "Zlib_jll"] +git-tree-sha1 = "47ce50b742921377301e15005c96e979574e130b" +uuid = "7746bdde-850d-59dc-9ae8-88ece973131d" +version = "2.68.1+0" + +[[Grisu]] +git-tree-sha1 = "53bb909d1151e57e2484c3d1b53e19552b887fb2" +uuid = "42e2da0e-8278-4e71-bc24-59509adca0fe" +version = "1.0.2" + +[[HTTP]] +deps = ["Base64", "Dates", "IniFile", "MbedTLS", "NetworkOptions", "Sockets", "URIs"] +git-tree-sha1 = "86ed84701fbfd1142c9786f8e53c595ff5a4def9" +uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3" +version = "0.9.10" + +[[IniFile]] +deps = ["Test"] +git-tree-sha1 = "098e4d2c533924c921f9f9847274f2ad89e018b8" +uuid = "83e8ac13-25f8-5344-8a64-a9f2b223428f" +version = "0.5.0" + +[[InteractiveUtils]] +deps = ["Markdown"] +uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" + +[[IterTools]] +git-tree-sha1 = "05110a2ab1fc5f932622ffea2a003221f4782c18" +uuid = "c8e1da08-722c-5040-9ed9-7db0dc04731e" +version = "1.3.0" + +[[IteratorInterfaceExtensions]] +git-tree-sha1 = "a3f24677c21f5bbe9d2a714f95dcd58337fb2856" +uuid = "82899510-4779-5014-852e-03e436cf321d" +version = "1.0.0" + +[[JLLWrappers]] +deps = ["Preferences"] +git-tree-sha1 = "642a199af8b68253517b80bd3bfd17eb4e84df6e" +uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" +version = "1.3.0" + +[[JSON]] +deps = ["Dates", "Mmap", "Parsers", "Unicode"] +git-tree-sha1 = "81690084b6198a2e1da36fcfda16eeca9f9f24e4" +uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +version = "0.21.1" + +[[JpegTurbo_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "d735490ac75c5cb9f1b00d8b5509c11984dc6943" +uuid = "aacddb02-875f-59d6-b918-886e6ef4fbf8" +version = "2.1.0+0" + +[[LAME_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "f6250b16881adf048549549fba48b1161acdac8c" +uuid = "c1c5ebd0-6772-5130-a774-d5fcae4a789d" +version = "3.100.1+0" + +[[LZO_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "e5b909bcf985c5e2605737d2ce278ed791b89be6" +uuid = "dd4b983a-f0e5-5f8d-a1b7-129d4a5fb1ac" +version = "2.10.1+0" + +[[LaTeXStrings]] +git-tree-sha1 = "c7f1c695e06c01b95a67f0cd1d34994f3e7db104" +uuid = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f" +version = "1.2.1" + +[[Latexify]] +deps = ["Formatting", "InteractiveUtils", "LaTeXStrings", "MacroTools", "Markdown", "Printf", "Requires"] +git-tree-sha1 = "a4b12a1bd2ebade87891ab7e36fdbce582301a92" +uuid = "23fbe1c1-3f47-55db-b15f-69d7ec21a316" +version = "0.15.6" + +[[LibCURL]] +deps = ["LibCURL_jll", "MozillaCACerts_jll"] +uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" + +[[LibCURL_jll]] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "MbedTLS_jll", "Zlib_jll", "nghttp2_jll"] +uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" + +[[LibGit2]] +deps = ["Base64", "NetworkOptions", "Printf", "SHA"] +uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" + +[[LibSSH2_jll]] +deps = ["Artifacts", "Libdl", "MbedTLS_jll"] +uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" + +[[LibVPX_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "12ee7e23fa4d18361e7c2cde8f8337d4c3101bc7" +uuid = "dd192d2f-8180-539f-9fb4-cc70b1dcf69a" +version = "1.10.0+0" + +[[Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" + +[[Libffi_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "761a393aeccd6aa92ec3515e428c26bf99575b3b" +uuid = "e9f186c6-92d2-5b65-8a66-fee21dc1b490" +version = "3.2.2+0" + +[[Libgcrypt_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Libgpg_error_jll", "Pkg"] +git-tree-sha1 = "64613c82a59c120435c067c2b809fc61cf5166ae" +uuid = "d4300ac3-e22c-5743-9152-c294e39db1e4" +version = "1.8.7+0" + +[[Libglvnd_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_libX11_jll", "Xorg_libXext_jll"] +git-tree-sha1 = "7739f837d6447403596a75d19ed01fd08d6f56bf" +uuid = "7e76a0d4-f3c7-5321-8279-8d96eeed0f29" +version = "1.3.0+3" + +[[Libgpg_error_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "c333716e46366857753e273ce6a69ee0945a6db9" +uuid = "7add5ba3-2f88-524e-9cd5-f83b8a55f7b8" +version = "1.42.0+0" + +[[Libiconv_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "8d22e127ea9a0917bc98ebd3755c8bd31989381e" +uuid = "94ce4f54-9a6c-5748-9c1c-f9c7231a4531" +version = "1.16.1+0" + +[[Libmount_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "9c30530bf0effd46e15e0fdcf2b8636e78cbbd73" +uuid = "4b2f31a3-9ecc-558c-b454-b3730dcb73e9" +version = "2.35.0+0" + +[[Libtiff_jll]] +deps = ["Artifacts", "JLLWrappers", "JpegTurbo_jll", "Libdl", "Pkg", "Zlib_jll", "Zstd_jll"] +git-tree-sha1 = "340e257aada13f95f98ee352d316c3bed37c8ab9" +uuid = "89763e89-9b03-5906-acba-b20f662cd828" +version = "4.3.0+0" + +[[Libuuid_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "7f3efec06033682db852f8b3bc3c1d2b0a0ab066" +uuid = "38a345b3-de98-5d2b-a5d3-14cd9215e700" +version = "2.36.0+0" + +[[LinearAlgebra]] +deps = ["Libdl"] +uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" + +[[Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" + +[[MacroTools]] +deps = ["Markdown", "Random"] +git-tree-sha1 = "6a8a2a625ab0dea913aba95c11370589e0239ff0" +uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" +version = "0.5.6" + +[[Markdown]] +deps = ["Base64"] +uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" + +[[MbedTLS]] +deps = ["Dates", "MbedTLS_jll", "Random", "Sockets"] +git-tree-sha1 = "1c38e51c3d08ef2278062ebceade0e46cefc96fe" +uuid = "739be429-bea8-5141-9913-cc70e7f3736d" +version = "1.0.3" + +[[MbedTLS_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" + +[[Measures]] +git-tree-sha1 = "e498ddeee6f9fdb4551ce855a46f54dbd900245f" +uuid = "442fdcdd-2543-5da2-b0f3-8c86c306513e" +version = "0.3.1" + +[[Missings]] +deps = ["DataAPI"] +git-tree-sha1 = "4ea90bd5d3985ae1f9a908bd4500ae88921c5ce7" +uuid = "e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28" +version = "1.0.0" + +[[Mmap]] +uuid = "a63ad114-7e13-5084-954f-fe012c677804" + +[[MozillaCACerts_jll]] +uuid = "14a3606d-f60d-562e-9121-12d972cd8159" + +[[NaNMath]] +git-tree-sha1 = "bfe47e760d60b82b66b61d2d44128b62e3a369fb" +uuid = "77ba4419-2d1f-58cd-9bb1-8ffee604a2e3" +version = "0.3.5" + +[[NetworkOptions]] +uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" + +[[Ogg_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "7937eda4681660b4d6aeeecc2f7e1c81c8ee4e2f" +uuid = "e7412a2a-1a6e-54c0-be00-318e2571c051" +version = "1.3.5+0" + +[[OpenSSL_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "15003dcb7d8db3c6c857fda14891a539a8f2705a" +uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" +version = "1.1.10+0" + +[[Opus_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "51a08fb14ec28da2ec7a927c4337e4332c2a4720" +uuid = "91d4177d-7536-5919-b921-800302f37372" +version = "1.3.2+0" + +[[OrderedCollections]] +git-tree-sha1 = "85f8e6578bf1f9ee0d11e7bb1b1456435479d47c" +uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +version = "1.4.1" + +[[PCRE_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "b2a7af664e098055a7529ad1a900ded962bca488" +uuid = "2f80f16e-611a-54ab-bc61-aa92de5b98fc" +version = "8.44.0+0" + +[[Parsers]] +deps = ["Dates"] +git-tree-sha1 = "c8abc88faa3f7a3950832ac5d6e690881590d6dc" +uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" +version = "1.1.0" + +[[Pixman_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "b4f5d02549a10e20780a24fce72bea96b6329e29" +uuid = "30392449-352a-5448-841d-b1acce4e97dc" +version = "0.40.1+0" + +[[Pkg]] +deps = ["Artifacts", "Dates", "Downloads", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"] +uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" + +[[PlotThemes]] +deps = ["PlotUtils", "Requires", "Statistics"] +git-tree-sha1 = "a3a964ce9dc7898193536002a6dd892b1b5a6f1d" +uuid = "ccf2f8ad-2431-5c83-bf29-c5338b663b6a" +version = "2.0.1" + +[[PlotUtils]] +deps = ["ColorSchemes", "Colors", "Dates", "Printf", "Random", "Reexport", "Statistics"] +git-tree-sha1 = "ae9a295ac761f64d8c2ec7f9f24d21eb4ffba34d" +uuid = "995b91a9-d308-5afd-9ec6-746e21dbc043" +version = "1.0.10" + +[[Plots]] +deps = ["Base64", "Contour", "Dates", "FFMPEG", "FixedPointNumbers", "GR", "GeometryBasics", "JSON", "Latexify", "LinearAlgebra", "Measures", "NaNMath", "PlotThemes", "PlotUtils", "Printf", "REPL", "Random", "RecipesBase", "RecipesPipeline", "Reexport", "Requires", "Scratch", "Showoff", "SparseArrays", "Statistics", "StatsBase", "UUIDs"] +git-tree-sha1 = "e69bc8f4728cb6db3ac16e728dc52f9e5ab722b9" +uuid = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +version = "1.16.4" + +[[Preferences]] +deps = ["TOML"] +git-tree-sha1 = "00cfd92944ca9c760982747e9a1d0d5d86ab1e5a" +uuid = "21216c6a-2e73-6563-6e65-726566657250" +version = "1.2.2" + +[[Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[[Qt5Base_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Fontconfig_jll", "Glib_jll", "JLLWrappers", "Libdl", "Libglvnd_jll", "OpenSSL_jll", "Pkg", "Xorg_libXext_jll", "Xorg_libxcb_jll", "Xorg_xcb_util_image_jll", "Xorg_xcb_util_keysyms_jll", "Xorg_xcb_util_renderutil_jll", "Xorg_xcb_util_wm_jll", "Zlib_jll", "xkbcommon_jll"] +git-tree-sha1 = "ad368663a5e20dbb8d6dc2fddeefe4dae0781ae8" +uuid = "ea2cea3b-5b76-57ae-a6ef-0a8af62496e1" +version = "5.15.3+0" + +[[REPL]] +deps = ["InteractiveUtils", "Markdown", "Sockets", "Unicode"] +uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" + +[[Random]] +deps = ["Serialization"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" + +[[RecipesBase]] +git-tree-sha1 = "b3fb709f3c97bfc6e948be68beeecb55a0b340ae" +uuid = "3cdcf5f2-1ef4-517c-9805-6587b60abb01" +version = "1.1.1" + +[[RecipesPipeline]] +deps = ["Dates", "NaNMath", "PlotUtils", "RecipesBase"] +git-tree-sha1 = "7a5026a6741c14147d1cb6daf2528a77ca28eb51" +uuid = "01d81517-befc-4cb6-b9ec-a95719d0359c" +version = "0.3.2" + +[[Reexport]] +git-tree-sha1 = "5f6c21241f0f655da3952fd60aa18477cf96c220" +uuid = "189a3867-3050-52da-a836-e630ba90ab69" +version = "1.1.0" + +[[Requires]] +deps = ["UUIDs"] +git-tree-sha1 = "4036a3bd08ac7e968e27c203d45f5fff15020621" +uuid = "ae029012-a4dd-5104-9daa-d747884805df" +version = "1.1.3" + +[[SHA]] +uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" + +[[Scratch]] +deps = ["Dates"] +git-tree-sha1 = "0b4b7f1393cff97c33891da2a0bf69c6ed241fda" +uuid = "6c6a2e73-6563-6170-7368-637461726353" +version = "1.1.0" + +[[Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" + +[[SharedArrays]] +deps = ["Distributed", "Mmap", "Random", "Serialization"] +uuid = "1a1011a3-84de-559e-8e89-a11a2f7dc383" + +[[Showoff]] +deps = ["Dates", "Grisu"] +git-tree-sha1 = "91eddf657aca81df9ae6ceb20b959ae5653ad1de" +uuid = "992d4aef-0814-514b-bc4d-f2e9a6c4116f" +version = "1.0.3" + +[[Sockets]] +uuid = "6462fe0b-24de-5631-8697-dd941f90decc" + +[[SortingAlgorithms]] +deps = ["DataStructures"] +git-tree-sha1 = "2ec1962eba973f383239da22e75218565c390a96" +uuid = "a2af1166-a08f-5f64-846c-94a0d3cef48c" +version = "1.0.0" + +[[SparseArrays]] +deps = ["LinearAlgebra", "Random"] +uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" + +[[StaticArrays]] +deps = ["LinearAlgebra", "Random", "Statistics"] +git-tree-sha1 = "42378d3bab8b4f57aa1ca443821b752850592668" +uuid = "90137ffa-7385-5640-81b9-e52037218182" +version = "1.2.2" + +[[Statistics]] +deps = ["LinearAlgebra", "SparseArrays"] +uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" + +[[StatsAPI]] +git-tree-sha1 = "1958272568dc176a1d881acb797beb909c785510" +uuid = "82ae8749-77ed-4fe6-ae5f-f523153014b0" +version = "1.0.0" + +[[StatsBase]] +deps = ["DataAPI", "DataStructures", "LinearAlgebra", "Missings", "Printf", "Random", "SortingAlgorithms", "SparseArrays", "Statistics", "StatsAPI"] +git-tree-sha1 = "2f6792d523d7448bbe2fec99eca9218f06cc746d" +uuid = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" +version = "0.33.8" + +[[StructArrays]] +deps = ["Adapt", "DataAPI", "Tables"] +git-tree-sha1 = "44b3afd37b17422a62aea25f04c1f7e09ce6b07f" +uuid = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" +version = "0.5.1" + +[[TOML]] +deps = ["Dates"] +uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" + +[[TableTraits]] +deps = ["IteratorInterfaceExtensions"] +git-tree-sha1 = "c06b2f539df1c6efa794486abfb6ed2022561a39" +uuid = "3783bdb8-4a98-5b6b-af9a-565f29a5fe9c" +version = "1.0.1" + +[[Tables]] +deps = ["DataAPI", "DataValueInterfaces", "IteratorInterfaceExtensions", "LinearAlgebra", "TableTraits", "Test"] +git-tree-sha1 = "aa30f8bb63f9ff3f8303a06c604c8500a69aa791" +uuid = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" +version = "1.4.3" + +[[Tar]] +deps = ["ArgTools", "SHA"] +uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" + +[[Test]] +deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] +uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[[URIs]] +git-tree-sha1 = "97bbe755a53fe859669cd907f2d96aee8d2c1355" +uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" +version = "1.3.0" + +[[UUIDs]] +deps = ["Random", "SHA"] +uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" + +[[Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" + +[[Wayland_jll]] +deps = ["Artifacts", "Expat_jll", "JLLWrappers", "Libdl", "Libffi_jll", "Pkg", "XML2_jll"] +git-tree-sha1 = "dc643a9b774da1c2781413fd7b6dcd2c56bb8056" +uuid = "a2964d1f-97da-50d4-b82a-358c7fce9d89" +version = "1.17.0+4" + +[[Wayland_protocols_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Wayland_jll"] +git-tree-sha1 = "2839f1c1296940218e35df0bbb220f2a79686670" +uuid = "2381bf8a-dfd0-557d-9999-79630e7b1b91" +version = "1.18.0+4" + +[[XML2_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Libiconv_jll", "Pkg", "Zlib_jll"] +git-tree-sha1 = "1acf5bdf07aa0907e0a37d3718bb88d4b687b74a" +uuid = "02c8fc9c-b97f-50b9-bbe4-9be30ff0a78a" +version = "2.9.12+0" + +[[XSLT_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Libgcrypt_jll", "Libgpg_error_jll", "Libiconv_jll", "Pkg", "XML2_jll", "Zlib_jll"] +git-tree-sha1 = "91844873c4085240b95e795f692c4cec4d805f8a" +uuid = "aed1982a-8fda-507f-9586-7b0439959a61" +version = "1.1.34+0" + +[[Xorg_libX11_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_libxcb_jll", "Xorg_xtrans_jll"] +git-tree-sha1 = "5be649d550f3f4b95308bf0183b82e2582876527" +uuid = "4f6342f7-b3d2-589e-9d20-edeb45f2b2bc" +version = "1.6.9+4" + +[[Xorg_libXau_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "4e490d5c960c314f33885790ed410ff3a94ce67e" +uuid = "0c0b7dd1-d40b-584c-a123-a41640f87eec" +version = "1.0.9+4" + +[[Xorg_libXcursor_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_libXfixes_jll", "Xorg_libXrender_jll"] +git-tree-sha1 = "12e0eb3bc634fa2080c1c37fccf56f7c22989afd" +uuid = "935fb764-8cf2-53bf-bb30-45bb1f8bf724" +version = "1.2.0+4" + +[[Xorg_libXdmcp_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "4fe47bd2247248125c428978740e18a681372dd4" +uuid = "a3789734-cfe1-5b06-b2d0-1dd0d9d62d05" +version = "1.1.3+4" + +[[Xorg_libXext_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_libX11_jll"] +git-tree-sha1 = "b7c0aa8c376b31e4852b360222848637f481f8c3" +uuid = "1082639a-0dae-5f34-9b06-72781eeb8cb3" +version = "1.3.4+4" + +[[Xorg_libXfixes_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_libX11_jll"] +git-tree-sha1 = "0e0dc7431e7a0587559f9294aeec269471c991a4" +uuid = "d091e8ba-531a-589c-9de9-94069b037ed8" +version = "5.0.3+4" + +[[Xorg_libXi_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_libXext_jll", "Xorg_libXfixes_jll"] +git-tree-sha1 = "89b52bc2160aadc84d707093930ef0bffa641246" +uuid = "a51aa0fd-4e3c-5386-b890-e753decda492" +version = "1.7.10+4" + +[[Xorg_libXinerama_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_libXext_jll"] +git-tree-sha1 = "26be8b1c342929259317d8b9f7b53bf2bb73b123" +uuid = "d1454406-59df-5ea1-beac-c340f2130bc3" +version = "1.1.4+4" + +[[Xorg_libXrandr_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_libXext_jll", "Xorg_libXrender_jll"] +git-tree-sha1 = "34cea83cb726fb58f325887bf0612c6b3fb17631" +uuid = "ec84b674-ba8e-5d96-8ba1-2a689ba10484" +version = "1.5.2+4" + +[[Xorg_libXrender_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_libX11_jll"] +git-tree-sha1 = "19560f30fd49f4d4efbe7002a1037f8c43d43b96" +uuid = "ea2f1a96-1ddc-540d-b46f-429655e07cfa" +version = "0.9.10+4" + +[[Xorg_libpthread_stubs_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "6783737e45d3c59a4a4c4091f5f88cdcf0908cbb" +uuid = "14d82f49-176c-5ed1-bb49-ad3f5cbd8c74" +version = "0.1.0+3" + +[[Xorg_libxcb_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "XSLT_jll", "Xorg_libXau_jll", "Xorg_libXdmcp_jll", "Xorg_libpthread_stubs_jll"] +git-tree-sha1 = "daf17f441228e7a3833846cd048892861cff16d6" +uuid = "c7cfdc94-dc32-55de-ac96-5a1b8d977c5b" +version = "1.13.0+3" + +[[Xorg_libxkbfile_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_libX11_jll"] +git-tree-sha1 = "926af861744212db0eb001d9e40b5d16292080b2" +uuid = "cc61e674-0454-545c-8b26-ed2c68acab7a" +version = "1.1.0+4" + +[[Xorg_xcb_util_image_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_xcb_util_jll"] +git-tree-sha1 = "0fab0a40349ba1cba2c1da699243396ff8e94b97" +uuid = "12413925-8142-5f55-bb0e-6d7ca50bb09b" +version = "0.4.0+1" + +[[Xorg_xcb_util_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_libxcb_jll"] +git-tree-sha1 = "e7fd7b2881fa2eaa72717420894d3938177862d1" +uuid = "2def613f-5ad1-5310-b15b-b15d46f528f5" +version = "0.4.0+1" + +[[Xorg_xcb_util_keysyms_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_xcb_util_jll"] +git-tree-sha1 = "d1151e2c45a544f32441a567d1690e701ec89b00" +uuid = "975044d2-76e6-5fbe-bf08-97ce7c6574c7" +version = "0.4.0+1" + +[[Xorg_xcb_util_renderutil_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_xcb_util_jll"] +git-tree-sha1 = "dfd7a8f38d4613b6a575253b3174dd991ca6183e" +uuid = "0d47668e-0667-5a69-a72c-f761630bfb7e" +version = "0.3.9+1" + +[[Xorg_xcb_util_wm_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_xcb_util_jll"] +git-tree-sha1 = "e78d10aab01a4a154142c5006ed44fd9e8e31b67" +uuid = "c22f9ab0-d5fe-5066-847c-f4bb1cd4e361" +version = "0.4.1+1" + +[[Xorg_xkbcomp_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_libxkbfile_jll"] +git-tree-sha1 = "4bcbf660f6c2e714f87e960a171b119d06ee163b" +uuid = "35661453-b289-5fab-8a00-3d9160c6a3a4" +version = "1.4.2+4" + +[[Xorg_xkeyboard_config_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Xorg_xkbcomp_jll"] +git-tree-sha1 = "5c8424f8a67c3f2209646d4425f3d415fee5931d" +uuid = "33bec58e-1273-512f-9401-5d533626f822" +version = "2.27.0+4" + +[[Xorg_xtrans_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "79c31e7844f6ecf779705fbc12146eb190b7d845" +uuid = "c5fb5394-a638-5e4d-96e5-b29de1b5cf10" +version = "1.4.0+3" + +[[Zlib_jll]] +deps = ["Libdl"] +uuid = "83775a58-1f1d-513f-b197-d71354ab007a" + +[[Zstd_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "cc4bf3fdde8b7e3e9fa0351bdeedba1cf3b7f6e6" +uuid = "3161d3a3-bdf6-5164-811a-617609db77b4" +version = "1.5.0+0" + +[[libass_jll]] +deps = ["Artifacts", "Bzip2_jll", "FreeType2_jll", "FriBidi_jll", "JLLWrappers", "Libdl", "Pkg", "Zlib_jll"] +git-tree-sha1 = "acc685bcf777b2202a904cdcb49ad34c2fa1880c" +uuid = "0ac62f75-1d6f-5e53-bd7c-93b484bb37c0" +version = "0.14.0+4" + +[[libfdk_aac_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "7a5780a0d9c6864184b3a2eeeb833a0c871f00ab" +uuid = "f638f0a6-7fb0-5443-88ba-1cc74229b280" +version = "0.1.6+4" + +[[libpng_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Zlib_jll"] +git-tree-sha1 = "94d180a6d2b5e55e447e2d27a29ed04fe79eb30c" +uuid = "b53b4c65-9356-5827-b1ea-8c7a1a84506f" +version = "1.6.38+0" + +[[libvorbis_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Ogg_jll", "Pkg"] +git-tree-sha1 = "c45f4e40e7aafe9d086379e5578947ec8b95a8fb" +uuid = "f27f6e37-5d2b-51aa-960f-b287f2bc3b7a" +version = "1.3.7+0" + +[[nghttp2_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" + +[[p7zip_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" + +[[x264_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "d713c1ce4deac133e3334ee12f4adff07f81778f" +uuid = "1270edf5-f2f9-52d2-97e9-ab00b5d0237a" +version = "2020.7.14+2" + +[[x265_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "487da2f8f2f0c8ee0e83f39d13037d6bbf0a45ab" +uuid = "dfaa095f-4041-5dcd-9319-2fabd8486b76" +version = "3.0.0+3" + +[[xkbcommon_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg", "Wayland_jll", "Wayland_protocols_jll", "Xorg_libxcb_jll", "Xorg_xkeyboard_config_jll"] +git-tree-sha1 = "ece2350174195bb31de1a63bea3a41ae1aa593b6" +uuid = "d8fb68d0-12a3-5cfd-a85a-d49703b185fd" +version = "0.9.1+5" +""" + # ╔═╡ Cell order: # ╟─7b93882c-9ad8-11ea-0288-0941e163f9d5 -# ╟─d889e13e-f0ff-11ea-3306-57491904d83f -# ╠═aefb6004-f0ff-11ea-10c9-c504c7aa9fe5 -# ╟─e48a0e78-f0ff-11ea-2852-2f4eee1a7d2d -# ╠═a0f0ae42-9ad8-11ea-2475-a735df64aa28 -# ╟─a9a3c640-f100-11ea-2e86-5f0e81a37ebd -# ╟─c042d3d2-f100-11ea-3833-af1f19afd400 +# ╠═5e611449-c239-4547-9d13-5db33a081e58 # ╟─e5abaf40-f105-11ea-1494-2da858d7db5b # ╠═9414a092-f105-11ea-10cd-23f84e47d876 # ╟─5ae65950-9ad9-11ea-2e14-35119d369acd @@ -278,3 +1063,5 @@ Try changing the function for the base plot, or the addition in the bottom cell. # ╠═9e4d1462-9ae4-11ea-3c0f-774d68a671c3 # ╠═a7906786-9ae4-11ea-0175-4f860c05a8a2 # ╟─d3cbc548-9ae4-11ea-261f-7fb956839e53 +# ╟─00000000-0000-0000-0000-000000000001 +# ╟─00000000-0000-0000-0000-000000000002 diff --git a/sample/PlutoUI.jl.jl b/sample/PlutoUI.jl.jl index ba4ff8f57a..24dfc22498 100644 --- a/sample/PlutoUI.jl.jl +++ b/sample/PlutoUI.jl.jl @@ -1,5 +1,5 @@ ### A Pluto.jl notebook ### -# v0.12.3 +# v0.15.0 using Markdown using InteractiveUtils @@ -13,17 +13,8 @@ macro bind(def, element) end end -# ╔═╡ ae0936ee-c767-11ea-0cbc-3f58779113da -begin - let - env = mktempdir() - import Pkg - Pkg.activate(env) - Pkg.Registry.update() - Pkg.add(Pkg.PackageSpec(;name="PlutoUI", version="0.6.7-0.6")) - end - using PlutoUI -end +# ╔═╡ 071d9ca5-9b42-4583-ad96-a48f93453a0e +using PlutoUI # ╔═╡ bc532cd2-c75b-11ea-313f-8b5e771c9227 md"""# PlutoUI.jl @@ -47,15 +38,7 @@ is hard to memorize, so `PlutoUI` makes it more _Julian_: # ╔═╡ 051f31fc-cc63-11ea-1e2c-0704285ea6a9 md""" #### To use it in other notebooks -First, add the `PlutoUI` package. The easiest way is to open a new Julia terminal, and type: -``` -julia> ] -(@v1.5) pkg> add PlutoUI -``` -Next, add this cell to your notebook: -```julia -using PlutoUI -``` +Simply import the `PlutoUI` package, and Pluto's built-in package manager takes care of the rest! """ # ╔═╡ fddb794c-c75c-11ea-1f55-eb9c178424cd @@ -478,20 +461,88 @@ space # ╔═╡ d163f434-cc5a-11ea-19e9-9319ba994efa space -# ╔═╡ 7242e236-cc4c-11ea-06d9-c1825cfe1fea -md"#### Footnote about package environments +# ╔═╡ 00000000-0000-0000-0000-000000000001 +PLUTO_PROJECT_TOML_CONTENTS = """ +[deps] +PlutoUI = "7f904dfe-b85e-4ff6-b463-dae2292396a8" + +[compat] +PlutoUI = "^0.7.9" +""" + +# ╔═╡ 00000000-0000-0000-0000-000000000002 +PLUTO_MANIFEST_TOML_CONTENTS = """ +# This file is machine-generated - editing it directly is not advised + +[[Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" + +[[Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" -`PlutoUI` needs to be installed for this sample notebook, but we don't want to add it to your global package environment. (Although you should install PlutoUI if you find it useful! It's a tiny package.) +[[InteractiveUtils]] +deps = ["Markdown"] +uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" -The solution is to add it into a _temporary environment_, which only exists inside this notebook. If you already had PlutoUI installed, Julia will re-use that installation from the global package cache." +[[JSON]] +deps = ["Dates", "Mmap", "Parsers", "Unicode"] +git-tree-sha1 = "81690084b6198a2e1da36fcfda16eeca9f9f24e4" +uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +version = "0.21.1" -# ╔═╡ 7aabca4a-cc4d-11ea-1f7d-e9e2fd901b81 -md"In the future, Pluto might do this automatically for all packages. The goal is for **each notebook to contain its own package information**, but we are still thinking about the best approach. If you are interested, have a look at the [GitHub issue](https://github.com/fonsp/Pluto.jl/issues/142), our [design doc](https://www.notion.so/Self-contained-reproducibility-995ffa5174894c26b897827bd2ce4990), or contact us using the suggestion box below!" +[[Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" + +[[Markdown]] +deps = ["Base64"] +uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" + +[[Mmap]] +uuid = "a63ad114-7e13-5084-954f-fe012c677804" + +[[Parsers]] +deps = ["Dates"] +git-tree-sha1 = "c8abc88faa3f7a3950832ac5d6e690881590d6dc" +uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" +version = "1.1.0" + +[[PlutoUI]] +deps = ["Base64", "Dates", "InteractiveUtils", "JSON", "Logging", "Markdown", "Random", "Reexport", "Suppressor"] +git-tree-sha1 = "44e225d5837e2a2345e69a1d1e01ac2443ff9fcb" +uuid = "7f904dfe-b85e-4ff6-b463-dae2292396a8" +version = "0.7.9" + +[[Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[[Random]] +deps = ["Serialization"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" + +[[Reexport]] +git-tree-sha1 = "5f6c21241f0f655da3952fd60aa18477cf96c220" +uuid = "189a3867-3050-52da-a836-e630ba90ab69" +version = "1.1.0" + +[[Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" + +[[Suppressor]] +git-tree-sha1 = "a819d77f31f83e5792a76081eee1ea6342ab8787" +uuid = "fd094767-a336-5f1f-9728-57cf17d0bbfb" +version = "0.2.0" + +[[Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" +""" # ╔═╡ Cell order: # ╟─bc532cd2-c75b-11ea-313f-8b5e771c9227 # ╟─3eff9592-cc63-11ea-2b61-4170d1a7656a # ╟─051f31fc-cc63-11ea-1e2c-0704285ea6a9 +# ╠═071d9ca5-9b42-4583-ad96-a48f93453a0e # ╟─fb6142f6-c765-11ea-29fd-7ff4e823c02b # ╟─fddb794c-c75c-11ea-1f55-eb9c178424cd # ╟─1e10d82a-c766-11ea-0c4b-3bd77f62759d @@ -598,6 +649,5 @@ md"In the future, Pluto might do this automatically for all packages. The goal i # ╟─d163f434-cc5a-11ea-19e9-9319ba994efa # ╟─512fe760-cc4c-11ea-1c5b-2b32da035aad # ╠═55bcdbf8-cc4c-11ea-1549-87c076a59ff4 -# ╟─7242e236-cc4c-11ea-06d9-c1825cfe1fea -# ╠═ae0936ee-c767-11ea-0cbc-3f58779113da -# ╟─7aabca4a-cc4d-11ea-1f7d-e9e2fd901b81 +# ╟─00000000-0000-0000-0000-000000000001 +# ╟─00000000-0000-0000-0000-000000000002 diff --git a/sample/old_notebook_with_using.jl b/sample/old_notebook_with_using.jl new file mode 100644 index 0000000000..ff7acdee30 --- /dev/null +++ b/sample/old_notebook_with_using.jl @@ -0,0 +1,28 @@ +### A Pluto.jl notebook ### +# v0.12.21 + +using Markdown +using InteractiveUtils + +# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error). +macro bind(def, element) + quote + local el = $(esc(element)) + global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : missing + el + end +end + +# ╔═╡ 7d550d46-7dc4-11eb-3df0-db7ad0594043 +using PlutoUI + +# ╔═╡ 8399ed34-7dc4-11eb-16ec-e572834e149d +@bind x Slider(1:10) + +# ╔═╡ 88c83d42-7dc4-11eb-1b5e-a1ecad4b6ff1 +x + +# ╔═╡ Cell order: +# ╠═7d550d46-7dc4-11eb-3df0-db7ad0594043 +# ╠═8399ed34-7dc4-11eb-16ec-e572834e149d +# ╠═88c83d42-7dc4-11eb-1b5e-a1ecad4b6ff1 diff --git a/src/Configuration.jl b/src/Configuration.jl index 8373632489..b8a5fa5e65 100644 --- a/src/Configuration.jl +++ b/src/Configuration.jl @@ -47,6 +47,7 @@ The HTTP server options. See [`SecurityOptions`](@ref) for additional settings. notebook_path_suggestion::String = notebook_path_suggestion() disable_writing_notebook_files::Bool = false notebook::Union{Nothing,String} = nothing + init_with_file_viewer::Bool=false simulated_lag::Real=0.0 end diff --git a/src/Pluto.jl b/src/Pluto.jl index ce45d8d6e7..1481543512 100644 --- a/src/Pluto.jl +++ b/src/Pluto.jl @@ -26,6 +26,7 @@ include("./evaluation/Tokens.jl") include("./runner/PlutoRunner.jl") include("./analysis/ExpressionExplorer.jl") include("./analysis/ReactiveNode.jl") +include("./packages/PkgCompat.jl") include("./notebook/Cell.jl") include("./analysis/Topology.jl") @@ -42,6 +43,8 @@ include("./analysis/TopologyUpdate.jl") include("./analysis/DependencyCache.jl") include("./evaluation/WorkspaceManager.jl") +include("./packages/Packages.jl") +include("./packages/PkgUtils.jl") include("./evaluation/Run.jl") include("./webserver/MsgPack.jl") @@ -51,6 +54,13 @@ include("./webserver/Dynamic.jl") include("./webserver/REPLTools.jl") include("./webserver/WebServer.jl") +const reset_notebook_environment = PkgUtils.reset_notebook_environment +const update_notebook_environment = PkgUtils.update_notebook_environment +const activate_notebook_environment = PkgUtils.activate_notebook_environment +export reset_notebook_environment +export update_notebook_environment +export activate_notebook_environment + if get(ENV, "JULIA_PLUTO_SHOW_BANNER", "1") !== "0" @info """\n Welcome to Pluto $(PLUTO_VERSION_STR) 🎈 diff --git a/src/analysis/TopologyUpdate.jl b/src/analysis/TopologyUpdate.jl index 319b8cf210..da95da8eb5 100644 --- a/src/analysis/TopologyUpdate.jl +++ b/src/analysis/TopologyUpdate.jl @@ -26,7 +26,6 @@ function updated_topology(old_topology::NotebookTopology, notebook::Notebook, ce new_codes = merge(old_topology.codes, updated_codes) new_nodes = merge(old_topology.nodes, updated_nodes) - # DONE (performance): deleted cells should not stay in the topology for removed_cell in setdiff(keys(old_topology.nodes), notebook.cells) delete!(new_nodes, removed_cell) delete!(new_codes, removed_cell) diff --git a/src/evaluation/Run.jl b/src/evaluation/Run.jl index 26bbf71670..aef0c598b9 100644 --- a/src/evaluation/Run.jl +++ b/src/evaluation/Run.jl @@ -111,10 +111,14 @@ end run_reactive_async!(session::ServerSession, notebook::Notebook, to_run::Vector{Cell}; kwargs...) = run_reactive_async!(session, notebook, notebook.topology, notebook.topology, to_run; kwargs...) function run_reactive_async!(session::ServerSession, notebook::Notebook, old::NotebookTopology, new::NotebookTopology, to_run::Vector{Cell}; run_async::Bool=true, kwargs...) - run_task = @async begin + maybe_async(run_async) do run_reactive!(session, notebook, old, new, to_run; kwargs...) end - if run_async +end + +function maybe_async(f::Function, async::Bool) + run_task = @asynclog f() + if async run_task else fetch(run_task) @@ -206,8 +210,14 @@ function update_save_run!(session::ServerSession, notebook::Notebook, cells::Arr setdiff(cells, to_run_offline) end - if !(isempty(to_run_online) && session.options.evaluation.lazy_workspace_creation) && will_run_code(notebook) - run_reactive_async!(session, notebook, old, new, to_run_online; run_async=run_async, kwargs...) + pkg_task = @async sync_nbpkg(session, notebook; save=save) + + maybe_async(run_async) do + wait(pkg_task) + if !(isempty(to_run_online) && session.options.evaluation.lazy_workspace_creation) && will_run_code(notebook) + # not async because that would be double async + run_reactive_async!(session, notebook, old, new, to_run_online; run_async=false, kwargs...) + end end end diff --git a/src/evaluation/Tokens.jl b/src/evaluation/Tokens.jl index 17256b2302..1dca8452b9 100644 --- a/src/evaluation/Tokens.jl +++ b/src/evaluation/Tokens.jl @@ -17,8 +17,6 @@ function withtoken(f::Function, token::Token) take!(token) result = try f() - catch e - rethrow(e) finally put!(token) end diff --git a/src/evaluation/WorkspaceManager.jl b/src/evaluation/WorkspaceManager.jl index 59f5c09d43..1f274fd118 100644 --- a/src/evaluation/WorkspaceManager.jl +++ b/src/evaluation/WorkspaceManager.jl @@ -1,18 +1,22 @@ module WorkspaceManager import UUIDs: UUID import ..Pluto: Configuration, Notebook, Cell, ProcessStatus, ServerSession, ExpressionExplorer, pluto_filename, Token, withtoken, Promise, tamepath, project_relative_path, putnotebookupdates!, UpdateMessage +import ..Pluto.PkgCompat import ..Configuration: CompilerOptions, _merge_notebook_compiler_options, _resolve_notebook_project_path, _convert_to_flags import ..Pluto.ExpressionExplorer: FunctionName import ..PlutoRunner import Distributed "Contains the Julia process (in the sense of `Distributed.addprocs`) to evaluate code in. Each notebook gets at most one `Workspace` at any time, but it can also have no `Workspace` (it cannot `eval` code in this case)." -mutable struct Workspace +Base.@kwdef mutable struct Workspace pid::Integer - discarded::Bool + discarded::Bool=false log_channel::Distributed.RemoteChannel module_name::Symbol - dowork_token::Token + dowork_token::Token=Token() + nbpkg_was_active::Bool=false + original_LOAD_PATH::Vector{String}=String[] + original_ACTIVE_PROJECT::Union{Nothing,String}=nothing end "These expressions get evaluated whenever a new `Workspace` process is created." @@ -55,16 +59,50 @@ function make_workspace((session, notebook)::SN; force_offline::Bool=false)::Wor $(Distributed).RemoteChannel(() -> eval(:(Main.PlutoRunner.log_channel)), $pid) end) module_name = create_emptyworkspacemodule(pid) - workspace = Workspace(pid, false, log_channel, module_name, Token()) + + original_LOAD_PATH, original_ACTIVE_PROJECT = Distributed.remotecall_eval(Main, pid, :(Base.LOAD_PATH, Base.ACTIVE_PROJECT[])) + + workspace = Workspace(; + pid=pid, + log_channel=log_channel, + module_name=module_name, + original_LOAD_PATH=original_LOAD_PATH, + original_ACTIVE_PROJECT=original_ACTIVE_PROJECT, + ) @async start_relaying_logs((session, notebook), log_channel) cd_workspace(workspace, notebook.path) + use_nbpkg_environment((session, notebook), workspace) force_offline || (notebook.process_status = ProcessStatus.ready) - return workspace end +function use_nbpkg_environment((session, notebook)::SN, workspace=nothing) + enabled = notebook.nbpkg_ctx !== nothing + if workspace.nbpkg_was_active == enabled + return + end + + workspace = workspace !== nothing ? workspace : get_workspace((session, notebook)) + if workspace.discarded + return + end + + workspace.nbpkg_was_active = enabled + if workspace.pid != Distributed.myid() + new_LP = enabled ? ["@", "@stdlib"] : workspace.original_LOAD_PATH + new_AP = enabled ? PkgCompat.env_dir(notebook.nbpkg_ctx) : workspace.original_ACTIVE_PROJECT + + Distributed.remotecall_eval(Main, [workspace.pid], quote + copy!(LOAD_PATH, $(new_LP)) + Base.ACTIVE_PROJECT[] = $(new_AP) + end) + else + # uhmmmmmm TODO + end +end + function start_relaying_logs((session, notebook)::SN, log_channel::Distributed.RemoteChannel) while true try @@ -193,8 +231,11 @@ function eval_format_fetch_in_workspace(session_notebook::Union{SN,Workspace}, e workspace = get_workspace(session_notebook) # if multiple notebooks run on the same process, then we need to `cd` between the different notebook paths - if workspace.pid == Distributed.myid() && session_notebook isa Tuple - cd_workspace(workspace, session_notebook[2].path) + if session_notebook isa Tuple + if workspace.pid == Distributed.myid() + cd_workspace(workspace, session_notebook[2].path) + end + use_nbpkg_environment(session_notebook, workspace) end # run the code 🏃‍♀️ diff --git a/src/notebook/Cell.jl b/src/notebook/Cell.jl index 1d981f3a68..01b9e82b3a 100644 --- a/src/notebook/Cell.jl +++ b/src/notebook/Cell.jl @@ -1,5 +1,5 @@ import UUIDs: UUID, uuid1 -import .ExpressionExplorer: SymbolsState +import .ExpressionExplorer: SymbolsState, UsingsImports Base.@kwdef struct CellOutput body::Union{Nothing,String,Vector{UInt8},Dict}=nothing diff --git a/src/notebook/Notebook.jl b/src/notebook/Notebook.jl index d863876c08..4326427b99 100644 --- a/src/notebook/Notebook.jl +++ b/src/notebook/Notebook.jl @@ -1,6 +1,8 @@ import UUIDs: UUID, uuid1 import .ExpressionExplorer: SymbolsState, FunctionNameSignaturePair, FunctionName import .Configuration +import .PkgCompat: PkgCompat, PkgContext +import Pkg mutable struct BondValue value::Any @@ -25,7 +27,7 @@ Base.@kwdef mutable struct Notebook cell_order::Array{UUID,1} path::String - notebook_id::UUID + notebook_id::UUID=uuid1() topology::NotebookTopology=NotebookTopology() _cached_topological_order::Union{Nothing,TopologicalOrder}=nothing @@ -39,6 +41,14 @@ Base.@kwdef mutable struct Notebook # per notebook compiler options # nothing means to use global session compiler options compiler_options::Union{Nothing,Configuration.CompilerOptions}=nothing + # nbpkg_ctx::Union{Nothing,PkgContext}=nothing + nbpkg_ctx::Union{Nothing,PkgContext}=PkgCompat.create_empty_ctx() + nbpkg_ctx_instantiated::Bool=false + nbpkg_restart_recommended_msg::Union{Nothing,String}=nothing + nbpkg_restart_required_msg::Union{Nothing,String}=nothing + nbpkg_terminal_outputs::Dict{String,String}=Dict{String,String}() + nbpkg_busy_packages::Vector{String}=String[] + nbpkg_installed_versions_cache::Dict{String,String}=Dict{String,String}() process_status::String=ProcessStatus.starting wants_to_interrupt::Bool=false @@ -79,6 +89,9 @@ const _order_delimiter = "# ╠═" const _order_delimiter_folded = "# ╟─" const _cell_suffix = "\n\n" +const _ptoml_cell_id = UUID(1) +const _mtoml_cell_id = UUID(2) + emptynotebook(args...) = Notebook([Cell()], args...) """ @@ -112,11 +125,46 @@ function save_notebook(io, notebook::Notebook) print(io, _cell_suffix) end + + using_plutopkg = notebook.nbpkg_ctx !== nothing + + write_package = if using_plutopkg + ptoml_path = joinpath(PkgCompat.env_dir(notebook.nbpkg_ctx), "Project.toml") + mtoml_path = joinpath(PkgCompat.env_dir(notebook.nbpkg_ctx), "Manifest.toml") + + ptoml_contents = isfile(ptoml_path) ? read(ptoml_path, String) : "" + mtoml_contents = isfile(mtoml_path) ? read(mtoml_path, String) : "" + + !isempty(ptoml_contents) || !isempty(mtoml_contents) + else + false + end + + if write_package + println(io, _cell_id_delimiter, string(_ptoml_cell_id)) + print(io, "PLUTO_PROJECT_TOML_CONTENTS = \"\"\"\n") + write(io, ptoml_contents) + print(io, "\"\"\"") + print(io, _cell_suffix) + + println(io, _cell_id_delimiter, string(_mtoml_cell_id)) + print(io, "PLUTO_MANIFEST_TOML_CONTENTS = \"\"\"\n") + write(io, mtoml_contents) + print(io, "\"\"\"") + print(io, _cell_suffix) + end + + println(io, _cell_id_delimiter, "Cell order:") for c in notebook.cells delim = c.code_folded ? _order_delimiter_folded : _order_delimiter println(io, delim, string(c.cell_id)) end + if write_package + println(io, _order_delimiter_folded, string(_ptoml_cell_id)) + println(io, _order_delimiter_folded, string(_mtoml_cell_id)) + end + notebook end @@ -148,7 +196,7 @@ function load_notebook_nobackup(io, path)::Notebook # @info "Loading a notebook saved with Pluto $(file_VERSION_STR). This is Pluto $(PLUTO_VERSION_STR)." end - collected_cells = Dict() + collected_cells = Dict{UUID,Cell}() # ignore first bits of file readuntil(io, _cell_id_delimiter) @@ -171,21 +219,62 @@ function load_notebook_nobackup(io, path)::Notebook end end - ordered_cells = Cell[] + cell_order = UUID[] while !eof(io) cell_id_str = String(readline(io)) - o, c = startswith(cell_id_str, _order_delimiter), if length(cell_id_str) >= 36 cell_id = let UUID(cell_id_str[end - 35:end]) end - next_cell = collected_cells[cell_id] - next_cell.code_folded = startswith(cell_id_str, _order_delimiter_folded) - push!(ordered_cells, next_cell) + next_cell = get(collected_cells, cell_id, nothing) + if next_cell !== nothing + next_cell.code_folded = startswith(cell_id_str, _order_delimiter_folded) + end + push!(cell_order, cell_id) + else + break end end - Notebook(ordered_cells, path) + read_package = + _ptoml_cell_id ∈ cell_order && + _mtoml_cell_id ∈ cell_order && + haskey(collected_cells, _ptoml_cell_id) && + haskey(collected_cells, _mtoml_cell_id) + + nbpkg_ctx = if read_package + ptoml_code = collected_cells[_ptoml_cell_id].code + mtoml_code = collected_cells[_mtoml_cell_id].code + + ptoml_contents = lstrip(split(ptoml_code, "\"\"\"")[2]) + mtoml_contents = lstrip(split(mtoml_code, "\"\"\"")[2]) + + env_dir = mktempdir() + write(joinpath(env_dir, "Project.toml"), ptoml_contents) + write(joinpath(env_dir, "Manifest.toml"), mtoml_contents) + + try + PkgCompat.load_ctx(env_dir) + catch e + @error "Failed to load notebook files: Project.toml+Manifest.toml parse error. Trying to recover Project.toml without Manifest.toml..." exception=(e,catch_backtrace()) + try + rm(joinpath(env_dir, "Manifest.toml")) + PkgCompat.load_ctx(env_dir) + catch e + @error "Failed to load notebook files: Project.toml parse error." exception=(e,catch_backtrace()) + PkgCompat.create_empty_ctx() + end + end + else + PkgCompat.create_empty_ctx() + end + + appeared_order = setdiff(cell_order ∩ keys(collected_cells), [_ptoml_cell_id, _mtoml_cell_id]) + appeared_cells_dict = filter(collected_cells) do (k, v) + k ∈ appeared_order + end + + Notebook(cells_dict=appeared_cells_dict, cell_order=appeared_order, path=path, nbpkg_ctx=nbpkg_ctx, nbpkg_installed_versions_cache=nbpkg_cache(nbpkg_ctx)) end function load_notebook_nobackup(path::String)::Notebook @@ -198,7 +287,7 @@ end "Create a backup of the given file, load the file as a .jl Pluto notebook, save the loaded notebook, compare the two files, and delete the backup of the newly saved file is equal to the backup." function load_notebook(path::String; disable_writing_notebook_files::Bool=false)::Notebook - backup_path = numbered_until_new(without_pluto_file_extension(path); sep=" backup ", suffix=".jl", create_file=false, skip_original=true) + backup_path = backup_filename(path) # local backup_num = 1 # backup_path = path # while isfile(backup_path) diff --git a/src/notebook/PathHelpers.jl b/src/notebook/PathHelpers.jl index 7d09eedc96..544a3f7ccf 100644 --- a/src/notebook/PathHelpers.jl +++ b/src/notebook/PathHelpers.jl @@ -123,6 +123,8 @@ function numbered_until_new(base::AbstractString; sep::AbstractString=" ", suffi chosen end +backup_filename(path) = numbered_until_new(without_pluto_file_extension(path); sep=" backup ", suffix=".jl", create_file=false, skip_original=true) + "Like `cp` except we create the file manually (to fix permission issues). (It's not plagiarism if you use this function to copy homework.)" function readwrite(from::AbstractString, to::AbstractString) write(to, read(from, String)) diff --git a/src/packages/Packages.jl b/src/packages/Packages.jl new file mode 100644 index 0000000000..ce5038ffc1 --- /dev/null +++ b/src/packages/Packages.jl @@ -0,0 +1,494 @@ + +import .ExpressionExplorer: external_package_names +import .PkgCompat +import .PkgCompat: select, is_stdlib + +const tiers = [ + Pkg.PRESERVE_ALL, + Pkg.PRESERVE_DIRECT, + Pkg.PRESERVE_SEMVER, + Pkg.PRESERVE_NONE, +] + +const pkg_token = Token() + + +function use_plutopkg(topology::NotebookTopology) + !any(values(topology.nodes)) do node + Symbol("Pkg.activate") ∈ node.references || + Symbol("Pkg.API.activate") ∈ node.references || + Symbol("Pkg.add") ∈ node.references || + Symbol("Pkg.API.add") ∈ node.references + end +end + +function external_package_names(topology::NotebookTopology)::Set{Symbol} + union!(Set{Symbol}(), external_package_names.(c.module_usings_imports for c in values(topology.codes))...) +end + + +PkgCompat.project_file(notebook::Notebook) = PkgCompat.project_file(PkgCompat.env_dir(notebook.nbpkg_ctx)) +PkgCompat.manifest_file(notebook::Notebook) = PkgCompat.manifest_file(PkgCompat.env_dir(notebook.nbpkg_ctx)) + + +""" +```julia +sync_nbpkg_core(notebook::Notebook; on_terminal_output::Function=((args...) -> nothing)) +``` + +Update the notebook package environment to match the notebook's code. This will: +- Add packages that should be added (because they are imported in a cell). +- Remove packages that are no longer needed. +- Make sure that the environment is instantiated. +- Detect the use of `Pkg.activate` and enable/disabled nbpkg accordingly. +""" +function sync_nbpkg_core(notebook::Notebook; on_terminal_output::Function=((args...) -> nothing)) + + 👺 = false + + use_plutopkg_old = notebook.nbpkg_ctx !== nothing + use_plutopkg_new = use_plutopkg(notebook.topology) + + if !use_plutopkg_old && use_plutopkg_new + @info "Started using PlutoPkg!! HELLO reproducibility!" + + 👺 = true + notebook.nbpkg_ctx = PkgCompat.create_empty_ctx() + end + if use_plutopkg_old && !use_plutopkg_new + @info "Stopped using PlutoPkg 💔😟😢" + + no_packages_loaded_yet = ( + notebook.nbpkg_restart_required_msg === nothing && + notebook.nbpkg_restart_recommended_msg === nothing && + all(PkgCompat.is_stdlib, keys(PkgCompat.project(notebook.nbpkg_ctx).dependencies)) + ) + 👺 = !no_packages_loaded_yet + notebook.nbpkg_ctx = nothing + end + + + if notebook.nbpkg_ctx !== nothing + PkgCompat.mark_original!(notebook.nbpkg_ctx) + + old_packages = String.(keys(PkgCompat.project(notebook.nbpkg_ctx).dependencies)) + new_packages = String.(external_package_names(notebook.topology)) # search all cells for imports and usings + + removed = setdiff(old_packages, new_packages) + added = setdiff(new_packages, old_packages) + + iolistener = let + busy_packages = notebook.nbpkg_ctx_instantiated ? added : new_packages + IOListener(callback=(s -> on_terminal_output(busy_packages, s))) + end + + # We remember which Pkg.Types.PreserveLevel was used. If it's too low, we will recommend/require a notebook restart later. + local used_tier = Pkg.PRESERVE_ALL + + if !isready(pkg_token) + println(iolistener.buffer, "Waiting for other notebooks to finish Pkg operations...") + trigger(iolistener) + end + + can_skip = isempty(removed) && isempty(added) && notebook.nbpkg_ctx_instantiated + + if !can_skip + return withtoken(pkg_token) do + PkgCompat.refresh_registry_cache() + + if !notebook.nbpkg_ctx_instantiated + notebook.nbpkg_ctx = PkgCompat.clear_stdlib_compat_entries(notebook.nbpkg_ctx) + PkgCompat.withio(notebook.nbpkg_ctx, IOContext(iolistener.buffer, :color => true)) do + withinteractive(false) do + try + Pkg.resolve(notebook.nbpkg_ctx) + catch e + @warn "Failed to resolve Pkg environment. Removing Manifest and trying again..." exception=e + reset_nbpkg(notebook; keep_project=true, save=false, backup=false) + Pkg.resolve(notebook.nbpkg_ctx) + end + end + end + end + + to_remove = filter(removed) do p + haskey(PkgCompat.project(notebook.nbpkg_ctx).dependencies, p) + end + if !isempty(to_remove) + @debug to_remove + # See later comment + mkeys() = Set(filter(!is_stdlib, [m.name for m in values(PkgCompat.dependencies(notebook.nbpkg_ctx))])) + old_manifest_keys = mkeys() + + Pkg.rm(notebook.nbpkg_ctx, [ + Pkg.PackageSpec(name=p) + for p in to_remove + ]) + + # We record the manifest before and after, to prevent recommending a reboot when nothing got removed from the manifest (e.g. when removing GR, but leaving Plots), or when only stdlibs got removed. + new_manifest_keys = mkeys() + + # TODO: we might want to upgrade other packages now that constraints have loosened? Does this happen automatically? + end + + + # TODO: instead of Pkg.PRESERVE_ALL, we actually want: + # "Pkg.PRESERVE_DIRECT, but preserve exact verisons of Base.loaded_modules" + + to_add = filter(PkgCompat.package_exists, added) + + if !isempty(to_add) + @debug to_add + startlistening(iolistener) + + PkgCompat.withio(notebook.nbpkg_ctx, IOContext(iolistener.buffer, :color => true)) do + withinteractive(false) do + # We temporarily clear the "semver-compatible" [deps] entries, because Pkg already respects semver, unless it doesn't, in which case we don't want to force it. + notebook.nbpkg_ctx = PkgCompat.clear_auto_compat_entries(notebook.nbpkg_ctx) + + try + for tier in [ + Pkg.PRESERVE_ALL, + Pkg.PRESERVE_DIRECT, + Pkg.PRESERVE_SEMVER, + Pkg.PRESERVE_NONE, + ] + used_tier = tier + + try + Pkg.add(notebook.nbpkg_ctx, [ + Pkg.PackageSpec(name=p) + for p in to_add + ]; preserve=used_tier) + + break + catch e + if used_tier == Pkg.PRESERVE_NONE + # give up + rethrow(e) + end + end + end + finally + notebook.nbpkg_ctx = PkgCompat.write_auto_compat_entries(notebook.nbpkg_ctx) + end + + # Now that Pkg is set up, the notebook process will call `using Package`, which can take some time. We write this message to the io, to notify the user. + println(iolistener.buffer, "\e[32m\e[1mLoading\e[22m\e[39m packages...") + end + end + + @debug "PlutoPkg done" + end + + should_instantiate = !notebook.nbpkg_ctx_instantiated || !isempty(to_add) || !isempty(to_remove) + if should_instantiate + startlistening(iolistener) + PkgCompat.withio(notebook.nbpkg_ctx, IOContext(iolistener.buffer, :color => true)) do + @debug "Instantiating" + + # Pkg.instantiate assumes that the environment to be instantiated is active, so we will have to modify the LOAD_PATH of this Pluto server + # We could also run the Pkg calls on the notebook process, but somehow I think that doing it on the server is more charming, though it requires this workaround. + env_dir = PkgCompat.env_dir(notebook.nbpkg_ctx) + pushfirst!(LOAD_PATH, env_dir) + + # update registries if this is the first time + PkgCompat.update_registries(notebook.nbpkg_ctx) + # instantiate without forcing registry update + PkgCompat.instantiate(notebook.nbpkg_ctx; update_registry=false) + + @assert LOAD_PATH[1] == env_dir + popfirst!(LOAD_PATH) + end + notebook.nbpkg_ctx_instantiated = true + end + + stoplistening(iolistener) + + return ( + did_something=👺 || ( + should_instantiate || (use_plutopkg_old != use_plutopkg_new) + ), + used_tier=used_tier, + # changed_versions=Dict{String,Pair}(), + restart_recommended=👺 || ( + (!isempty(to_remove) && old_manifest_keys != new_manifest_keys) || + used_tier != Pkg.PRESERVE_ALL + ), + restart_required=👺 || ( + used_tier ∈ [Pkg.PRESERVE_SEMVER, Pkg.PRESERVE_NONE] + ), + ) + end + end + end + return ( + did_something=👺 || (use_plutopkg_old != use_plutopkg_new), + used_tier=Pkg.PRESERVE_ALL, + # changed_versions=Dict{String,Pair}(), + restart_recommended=👺 || false, + restart_required=👺 || false, + ) +end + +""" +```julia +sync_nbpkg(session::ServerSession, notebook::Notebook; save::Bool=true) +``` + +In addition to the steps performed by [`sync_nbpkg_core`](@ref): +- Capture terminal outputs and store them in the `notebook` +- Update the clients connected to `notebook` +- `try` `catch` and reset the package environment on failure. +""" +function sync_nbpkg(session, notebook; save::Bool=true) + try + pkg_result = withtoken(notebook.executetoken) do + function iocallback(pkgs, s) + notebook.nbpkg_busy_packages = pkgs + for p in pkgs + notebook.nbpkg_terminal_outputs[p] = s + end + update_nbpkg_cache!(notebook) + send_notebook_changes!(ClientRequest(session=session, notebook=notebook)) + end + sync_nbpkg_core(notebook; on_terminal_output=iocallback) + end + + if pkg_result.did_something + @debug "PlutoPkg: success!" pkg_result + + if pkg_result.restart_recommended + @debug "PlutoPkg: Notebook restart recommended" + notebook.nbpkg_restart_recommended_msg = "yes" + end + if pkg_result.restart_required + @debug "PlutoPkg: Notebook restart REQUIRED" + notebook.nbpkg_restart_required_msg = "yes" + end + + notebook.nbpkg_busy_packages = String[] + update_nbpkg_cache!(notebook) + send_notebook_changes!(ClientRequest(session=session, notebook=notebook)) + save && save_notebook(notebook) + end + catch e + bt = catch_backtrace() + old_packages = try String.(keys(PkgCompat.project(notebook.nbpkg_ctx).dependencies)); catch; ["unknown"] end + new_packages = try String.(external_package_names(notebook.topology)); catch; ["unknown"] end + @warn """ + PlutoPkg: Failed to add/remove packages! Resetting package environment... + """ PLUTO_VERSION VERSION old_packages new_packages exception=(e, bt) + # TODO: send to user + + error_text = sprint(showerror, e, bt) + for p in notebook.nbpkg_busy_packages + old = get(notebook.nbpkg_terminal_outputs, p, "") + notebook.nbpkg_terminal_outputs[p] = old * "\n\n\nPkg error!\n\n" * error_text + end + notebook.nbpkg_busy_packages = String[] + update_nbpkg_cache!(notebook) + send_notebook_changes!(ClientRequest(session=session, notebook=notebook)) + + # Clear the embedded Project and Manifest and require a restart from the user. + reset_nbpkg(notebook; keep_project=false, save=save) + notebook.nbpkg_restart_required_msg = "yes" + notebook.nbpkg_ctx_instantiated = false + update_nbpkg_cache!(notebook) + send_notebook_changes!(ClientRequest(session=session, notebook=notebook)) + + save && save_notebook(notebook) + end +end + +function writebackup(notebook::Notebook) + backup_path = backup_filename(notebook.path) + Pluto.readwrite(notebook.path, backup_path) + + @info "Backup saved to" backup_path + + backup_path +end + +function reset_nbpkg(notebook::Notebook; keep_project::Bool=false, backup::Bool=true, save::Bool=true) + backup && save && writebackup(notebook) + + if notebook.nbpkg_ctx !== nothing + p = PkgCompat.project_file(notebook) + m = PkgCompat.manifest_file(notebook) + keep_project || (isfile(p) && rm(p)) + isfile(m) && rm(m) + + notebook.nbpkg_ctx = PkgCompat.load_ctx(PkgCompat.env_dir(notebook.nbpkg_ctx)) + else + notebook.nbpkg_ctx = use_plutopkg(notebook.topology) ? PkgCompat.create_empty_ctx() : nothing + end + + save && save_notebook(notebook) +end + +function update_nbpkg_core(notebook::Notebook; level::Pkg.UpgradeLevel=Pkg.UPLEVEL_MAJOR, on_terminal_output::Function=((args...) -> nothing)) + if notebook.nbpkg_ctx !== nothing + PkgCompat.mark_original!(notebook.nbpkg_ctx) + + old_packages = String.(keys(PkgCompat.project(notebook.nbpkg_ctx).dependencies)) + + iolistener = let + # we don't know which packages will be updated, so we send terminal output to all installed packages + IOListener(callback=(s -> on_terminal_output(old_packages, s))) + end + + # We remember which Pkg.Types.PreserveLevel was used. If it's too low, we will recommend/require a notebook restart later. + local used_tier = Pkg.PRESERVE_ALL + + if !isready(pkg_token) + println(iolistener.buffer, "Waiting for other notebooks to finish Pkg operations...") + trigger(iolistener) + end + + return withtoken(pkg_token) do + PkgCompat.refresh_registry_cache() + + if !notebook.nbpkg_ctx_instantiated + notebook.nbpkg_ctx = PkgCompat.clear_stdlib_compat_entries(notebook.nbpkg_ctx) + PkgCompat.withio(notebook.nbpkg_ctx, IOContext(iolistener.buffer, :color => true)) do + withinteractive(false) do + try + Pkg.resolve(notebook.nbpkg_ctx) + catch e + @warn "Failed to resolve Pkg environment. Removing Manifest and trying again..." exception=e + reset_nbpkg(notebook; keep_project=true, save=false, backup=false) + Pkg.resolve(notebook.nbpkg_ctx) + end + end + end + end + + startlistening(iolistener) + + PkgCompat.withio(notebook.nbpkg_ctx, IOContext(iolistener.buffer, :color => true)) do + # We temporarily clear the "semver-compatible" [deps] entries, because it is difficult to update them after the update 🙈. TODO + notebook.nbpkg_ctx = PkgCompat.clear_auto_compat_entries(notebook.nbpkg_ctx) + + try + ### + Pkg.update(notebook.nbpkg_ctx; level=level) + ### + finally + notebook.nbpkg_ctx = PkgCompat.write_auto_compat_entries(notebook.nbpkg_ctx) + end + end + + stoplistening(iolistener) + + 🐧 = !PkgCompat.is_original(notebook.nbpkg_ctx) + ( + did_something=🐧, + restart_recommended=🐧, + restart_required=🐧, + ) + end + end + ( + did_something=false, + restart_recommended=false, + restart_required=false, + ) +end + + +function update_nbpkg(session, notebook::Notebook; level::Pkg.UpgradeLevel=Pkg.UPLEVEL_MAJOR, backup::Bool=true, save::Bool=true) + if backup && save + bp = writebackup(notebook) + end + + try + pkg_result = withtoken(notebook.executetoken) do + original_outputs = deepcopy(notebook.nbpkg_terminal_outputs) + function iocallback(pkgs, s) + notebook.nbpkg_busy_packages = pkgs + for p in pkgs + original = get(original_outputs, p, "") + notebook.nbpkg_terminal_outputs[p] = original * "\n\n" * s + end + update_nbpkg_cache!(notebook) + send_notebook_changes!(ClientRequest(session=session, notebook=notebook)) + end + update_nbpkg_core(notebook; level=level, on_terminal_output=iocallback) + end + + if pkg_result.did_something + if pkg_result.restart_recommended + @debug "PlutoPkg: Notebook restart recommended" + notebook.nbpkg_restart_recommended_msg = "yes" + end + if pkg_result.restart_required + @debug "PlutoPkg: Notebook restart REQUIRED" + notebook.nbpkg_restart_required_msg = "yes" + end + else + isfile(bp) && rm(bp) + end + finally + notebook.nbpkg_busy_packages = String[] + update_nbpkg_cache!(notebook) + send_notebook_changes!(ClientRequest(session=session, notebook=notebook)) + save && save_notebook(notebook) + end +end + +nbpkg_cache(ctx::Union{Nothing,PkgContext}) = ctx === nothing ? Dict{String,String}() : Dict{String,String}( + x => string(PkgCompat.get_manifest_version(ctx, x)) for x in keys(PkgCompat.project(ctx).dependencies) +) + +function update_nbpkg_cache!(notebook::Notebook) + notebook.nbpkg_installed_versions_cache = nbpkg_cache(notebook.nbpkg_ctx) + notebook +end + +const is_interactive_defined = isdefined(Base, :is_interactive) && !Base.isconst(Base, :is_interactive) +function withinteractive(f::Function, value::Bool) + old_value = isinteractive() + @static if is_interactive_defined + Core.eval(Base, :(is_interactive = $value)) + end + try + f() + finally + @static if is_interactive_defined + Core.eval(Base, :(is_interactive = $old_value)) + end + end +end + +"A polling system to watch for writes to an IOBuffer. Up-to-date content will be passed as string to the `callback` function." +Base.@kwdef struct IOListener + callback::Function + buffer::IOBuffer=IOBuffer() + interval::Real=1.0/60 + running::Ref{Bool}=Ref(false) + last_size::Ref{Int}=Ref(-1) +end +function trigger(listener::IOListener) + new_size = listener.buffer.size + if new_size > listener.last_size[] + listener.last_size[] = new_size + new_contents = String(listener.buffer.data[1:new_size]) + listener.callback(new_contents) + end +end +function startlistening(listener::IOListener) + if !listener.running[] + listener.running[] = true + @async while listener.running[] + trigger(listener) + sleep(listener.interval) + end + end +end +function stoplistening(listener::IOListener) + if listener.running[] + listener.running[] = false + trigger(listener) + end +end diff --git a/src/packages/PkgCompat.jl b/src/packages/PkgCompat.jl new file mode 100644 index 0000000000..e2d4872862 --- /dev/null +++ b/src/packages/PkgCompat.jl @@ -0,0 +1,449 @@ +module PkgCompat + +export package_versions, package_completions + +import Pkg +import Pkg.Types: VersionRange + +import ..Pluto + +# Should be in Base +flatmap(args...) = vcat(map(args...)...) + +# Should be in Base +function select(f::Function, xs) + for x ∈ xs + if f(x) + return x + end + end + nothing +end + + +#= + +NOTE ABOUT PUBLIC/INTERNAL PKG API + +Pkg.jl exposes lots of API, but only some of it is "public": guaranteed to remain available. API is public if it is listed here: +https://pkgdocs.julialang.org/v1/api/ + +In this file, I labeled functions by their status using 🐸, ⚠️, etc. + +A status in brackets (like this) means that it is only called within this file, and the fallback might be in a caller function. + +--- + +I tried to only use public API, except: +- I use the `Pkg.Types.Context` value as first argument for many functions, since the server process manages multiple notebook processes, each with their own package environment. We could get rid of this, by settings `Base.ACTIVE_PROJECT[]` before and after each Pkg call. (This is temporarily activating the notebook environment.) This does have a performance impact, since the project and manifest caches are regenerated every time. +- https://github.com/JuliaLang/Pkg.jl/issues/2607 seems to be impossible with the current public API. +- Some functions try to use internal API for optimization/better features. + +=# + + + + + +### +# CONTEXT +### + + +const PkgContext = if isdefined(Pkg, :Context) + Pkg.Context +elseif isdefined(Pkg, :Types) && isdefined(Pkg.Types, :Context) + Pkg.Types.Context +elseif isdefined(Pkg, :API) && isdefined(Pkg.API, :Context) + Pkg.API.Context +else + Pkg.Types.Context +end + +# 🐸 "Public API", but using PkgContext +load_ctx(env_dir)::PkgContext = PkgContext(env=Pkg.Types.EnvCache(joinpath(env_dir, "Project.toml"))) + +# 🐸 "Public API", but using PkgContext +create_empty_ctx()::PkgContext = load_ctx(mktempdir()) + +# ⚠️ Internal API with fallback +function load_ctx(original::PkgContext) + new = load_ctx(env_dir(original)) + + try + new.env.original_project = original.env.original_project + new.env.original_manifest = original.env.original_manifest + catch e + @warn "Pkg compat: failed to set original_project" exception=(e,catch_backtrace()) + end + + new +end + +# ⚠️ Internal API with fallback +function mark_original!(ctx::PkgContext) + try + ctx.env.original_project = deepcopy(ctx.env.project) + ctx.env.original_manifest = deepcopy(ctx.env.manifest) + catch e + @warn "Pkg compat: failed to set original_project" exception=(e,catch_backtrace()) + end +end + +# ⚠️ Internal API with fallback +function is_original(ctx::PkgContext)::Bool + try + ctx.env.original_project == ctx.env.project && + ctx.env.original_manifest == ctx.env.manifest + catch e + @warn "Pkg compat: failed to get original_project" exception=(e,catch_backtrace()) + false + end +end + + + +# 🐸 "Public API", but using PkgContext +env_dir(ctx::PkgContext) = dirname(ctx.env.project_file) + +project_file(x::AbstractString) = joinpath(x, "Project.toml") +manifest_file(x::AbstractString) = joinpath(x, "Manifest.toml") +project_file(ctx::PkgContext) = joinpath(env_dir(ctx), "Project.toml") +manifest_file(ctx::PkgContext) = joinpath(env_dir(ctx), "Manifest.toml") + + +# ⚠️ Internal API with fallback +function withio(f::Function, ctx::PkgContext, io::IO) + @static if :io ∈ fieldnames(PkgContext) + old_io = ctx.io + ctx.io = io + result = try + f() + finally + ctx.io = old_io + nothing + end + result + else + f() + end +end + + +### +# REGISTRIES +### + +# (⛔️ Internal API) +"Return paths to all installed registries." +_get_registry_paths() = @static if isdefined(Pkg, :Types) && isdefined(Pkg.Types, :registries) + Pkg.Types.registries() +elseif isdefined(Pkg, :Registry) && isdefined(Pkg.Registry, :reachable_registries) + registry_specs = Pkg.Registry.reachable_registries() + [s.path for s in registry_specs] +elseif isdefined(Pkg, :Types) && isdefined(Pkg.Types, :collect_registries) + registry_specs = Pkg.Types.collect_registries() + [s.path for s in registry_specs] +else + String[] +end + +# (⛔️ Internal API) +_get_registries() = map(_get_registry_paths()) do r + @static if isdefined(Pkg, :Registry) && isdefined(Pkg.Registry, :RegistryInstance) + Pkg.Registry.RegistryInstance(r) + else + r => Pkg.Types.read_registry(joinpath(r, "Registry.toml")) + end +end + +# (⛔️ Internal API) +"Contains all registries as `Pkg.Types.Registry` structs." +const _parsed_registries = Ref(_get_registries()) + +# (⛔️ Internal API) +"Re-parse the installed registries from disk." +function refresh_registry_cache() + _parsed_registries[] = _get_registries() +end + +const _updated_registries_compat = Ref(false) + +# ⚠️✅ Internal API with good fallback +function update_registries(ctx) + @static if isdefined(Pkg, :Types) && isdefined(Pkg.Types, :update_registries) + Pkg.Types.update_registries(ctx) + else + if !_updated_registries_compat[] + _updated_registries_compat[] = true + Pkg.Registry.update() + end + end +end + +# ⚠️✅ Internal API with fallback +function instantiate(ctx; update_registry::Bool) + @static if hasmethod(Pkg.instantiate, Tuple{}, (:update_registry,)) + Pkg.instantiate(ctx; update_registry=update_registry) + else + Pkg.instantiate(ctx) + end +end + + + +# (⚠️ Internal API with fallback) +_stdlibs() = try + values(Pkg.Types.stdlibs()) +catch e + @warn "Pkg compat: failed to load standard libraries." exception=(e,catch_backtrace()) + + String["CRC32c", "Future", "Sockets", "MbedTLS_jll", "Random", "ArgTools", "libLLVM_jll", "GMP_jll", "Pkg", "Serialization", "LibSSH2_jll", "SHA", "OpenBLAS_jll", "REPL", "LibUV_jll", "nghttp2_jll", "Unicode", "Profile", "SparseArrays", "LazyArtifacts", "CompilerSupportLibraries_jll", "Base64", "Artifacts", "PCRE2_jll", "Printf", "p7zip_jll", "UUIDs", "Markdown", "TOML", "OpenLibm_jll", "Test", "MPFR_jll", "Mmap", "SuiteSparse", "LibGit2", "LinearAlgebra", "Logging", "NetworkOptions", "LibGit2_jll", "LibOSXUnwind_jll", "Dates", "LibUnwind_jll", "Libdl", "LibCURL_jll", "dSFMT_jll", "Distributed", "InteractiveUtils", "Downloads", "SharedArrays", "SuiteSparse_jll", "LibCURL", "Statistics", "Zlib_jll", "FileWatching", "DelimitedFiles", "Tar", "MozillaCACerts_jll"] +end + +# ⚠️ Internal API with fallback +is_stdlib(package_name::AbstractString) = package_name ∈ _stdlibs() + +global_ctx = PkgContext() + +### +# Package names +### + +# ⚠️ Internal API with fallback +function package_completions(partial_name::AbstractString)::Vector{String} + String[ + filter(s -> startswith(s, partial_name), collect(_stdlibs())); + _registered_package_completions(partial_name) + ] +end + +# (⚠️ Internal API with fallback) +function _registered_package_completions(partial_name::AbstractString)::Vector{String} + # compat + try + @static if hasmethod(Pkg.REPLMode.complete_remote_package, (String,)) + Pkg.REPLMode.complete_remote_package(partial_name) + else + Pkg.REPLMode.complete_remote_package(partial_name, 1, length(partial_name))[1] + end + catch e + @warn "Pkg compat: failed to autocomplete packages" exception=(e,catch_backtrace()) + String[] + end +end + +### +# Package versions +### + +# (⛔️ Internal API) +""" +Return paths to all found registry entries of a given package name. + +# Example +```julia +julia> Pluto.PkgCompat._registry_entries("Pluto") +1-element Vector{String}: + "/Users/fons/.julia/registries/General/P/Pluto" +``` +""" +function _registry_entries(package_name::AbstractString, registries::Vector=_parsed_registries[])::Vector{String} + flatmap(registries) do (rpath, r) + packages = values(r["packages"]) + String[ + joinpath(rpath, d["path"]) + for d in packages + if d["name"] == package_name + ] + end +end + +# (⛔️ Internal API) +function _package_versions_from_path(registry_entry_fullpath::AbstractString)::Vector{VersionNumber} + # compat + vd = @static if isdefined(Pkg, :Operations) && isdefined(Pkg.Operations, :load_versions) && hasmethod(Pkg.Operations.load_versions, (String,)) + Pkg.Operations.load_versions(registry_entry_fullpath) + else + Pkg.Operations.load_versions(PkgContext(), registry_entry_fullpath) + end + vd |> keys |> collect +end + +# ⚠️ Internal API with fallback +# See https://github.com/JuliaLang/Pkg.jl/issues/2607 +""" +Return all registered versions of the given package. Returns `["stdlib"]` for standard libraries, and a `Vector{VersionNumber}` for registered packages. +""" +function package_versions(package_name::AbstractString)::Vector + if is_stdlib(package_name) + ["stdlib"] + else + try + @static(if isdefined(Pkg, :Registry) && isdefined(Pkg.Registry, :uuids_from_name) + flatmap(_parsed_registries[]) do reg + uuids_with_name = Pkg.Registry.uuids_from_name(reg, package_name) + flatmap(uuids_with_name) do u + pkg = get(reg, u, nothing) + if pkg !== nothing + info = Pkg.Registry.registry_info(pkg) + collect(keys(info.version_info)) + else + [] + end + end + end + else + ps = _registry_entries(package_name) + flatmap(_package_versions_from_path, ps) + end) |> sort + catch e + @warn "Pkg compat: failed to get installable versions." exception=(e,catch_backtrace()) + ["latest"] + end + end +end + +# ⚠️ Internal API with fallback +"Does a package with this name exist in one of the installed registries?" +package_exists(package_name::AbstractString)::Bool = + package_versions(package_name) |> !isempty + +# 🐸 "Public API", but using PkgContext +function dependencies(ctx) + # Pkg.dependencies(ctx) should also work on 1.5, but there is some weird bug (run the tests without this patch). This is probably some Pkg bug that got fixed. + @static if VERSION < v"1.6.0-a" + ctx.env.manifest + else + try + # ctx.env.manifest + @static if hasmethod(Pkg.dependencies, (PkgContext,)) + Pkg.dependencies(ctx) + else + Pkg.dependencies(ctx.env) + end + catch e + @error """ + Pkg error: you might need to use + + Pluto.PkgUtils.reset_notebook_environment(notebook_path) + + to reset this notebook's environment. + + Before doing so, consider sending your notebook file to https://github.com/fonsp/Pluto.jl/issues together with the following info: + """ Pluto.PLUTO_VERSION VERSION exception=(e,catch_backtrace()) + + Dict() + end + end +end + +function project(ctx::PkgContext) + @static if hasmethod(Pkg.project, (PkgContext,)) + Pkg.project(ctx) + else + Pkg.project(ctx.env) + end +end + +# 🐸 "Public API", but using PkgContext +"Find a package in the manifest. Return `nothing` if not found." +_get_manifest_entry(ctx::PkgContext, package_name::AbstractString) = + select(e -> e.name == package_name, values(dependencies(ctx))) + +# ⚠️ Internal API with fallback +""" +Find a package in the manifest given its name, and return its installed version. Return `"stdlib"` for a standard library, and `nothing` if not found. +""" +function get_manifest_version(ctx::PkgContext, package_name::AbstractString) + if is_stdlib(package_name) + "stdlib" + else + entry = _get_manifest_entry(ctx, package_name) + entry === nothing ? nothing : entry.version + end +end + +### +# WRITING COMPAT ENTRIES +### + +# ✅ Public API +function _modify_compat(f!::Function, ctx::PkgContext)::PkgContext + toml = Pkg.TOML.parsefile(project_file(ctx)) + compat = get!(Dict, toml, "compat") + + f!(compat) + + isempty(compat) && delete!(toml, "compat") + + write(project_file(ctx), sprint() do io + Pkg.TOML.print(io, toml; sorted=true) + end) + + return load_ctx(ctx) +end + + +# ✅ Public API +""" +Add any missing [`compat`](https://pkgdocs.julialang.org/v1/compatibility/) entries to the `Project.toml` for all direct dependencies. This serves as a 'fallback' in case someone (with a different Julia version) opens your notebook without being able to load the `Manifest.toml`. Return the new `PkgContext`. + +The automatic compat entry is: `"~" * string(installed_version)`. +""" +function write_auto_compat_entries(ctx::PkgContext)::PkgContext + _modify_compat(ctx) do compat + for p in keys(project(ctx).dependencies) + if !haskey(compat, p) + m_version = get_manifest_version(ctx, p) + if m_version !== nothing && !is_stdlib(p) + compat[p] = "~" * string(m_version) + end + end + end + end +end + + +# ✅ Public API +""" +Remove any automatically-generated [`compat`](https://pkgdocs.julialang.org/v1/compatibility/) entries from the `Project.toml`. This will undo the effects of [`write_auto_compat_entries`](@ref) but leave other (e.g. manual) compat entries intact. Return the new `PkgContext`. +""" +function clear_auto_compat_entries(ctx::PkgContext)::PkgContext + if isfile(project_file(ctx)) + _modify_compat(ctx) do compat + for p in keys(compat) + m_version = get_manifest_version(ctx, p) + if m_version !== nothing && !is_stdlib(p) + if compat[p] == "~" * string(m_version) + delete!(compat, p) + end + end + end + end + else + ctx + end +end + +# ✅ Public API +""" +Remove any [`compat`](https://pkgdocs.julialang.org/v1/compatibility/) entries from the `Project.toml` for standard libraries. These entries are created when an old version of Julia uses a package that later became a standard library, like https://github.com/JuliaPackaging/Artifacts.jl. Return the new `PkgContext`. +""" +function clear_stdlib_compat_entries(ctx::PkgContext)::PkgContext + if isfile(project_file(ctx)) + _modify_compat(ctx) do compat + for p in keys(compat) + if is_stdlib(p) + @info "Removing compat entry for stdlib" p + delete!(compat, p) + end + end + end + else + ctx + end +end + + +end \ No newline at end of file diff --git a/src/packages/PkgUtils.jl b/src/packages/PkgUtils.jl new file mode 100644 index 0000000000..52f3d91662 --- /dev/null +++ b/src/packages/PkgUtils.jl @@ -0,0 +1,238 @@ +module PkgUtils + +import FileWatching +import Pkg +import ..Pluto +import ..Pluto: Notebook, save_notebook, load_notebook, load_notebook_nobackup, withtoken, Token, readwrite, PkgCompat +import ..Pluto.PkgCompat: project_file, manifest_file + +using Markdown + +export activate_notebook + +ensure_has_nbpkg(notebook::Notebook) = if notebook.nbpkg_ctx === nothing + + # TODO: update_save the notebook to init packages and stuff? + error(""" + This notebook is not using Pluto's package manager. This means that either: + 1. The notebook contains Pkg.activate or Pkg.add calls, or + 2. The notebook was created before Pluto 0.15. + + Open the notebook using Pluto to get started. + """) +else + for f in [notebook |> project_file, notebook |> manifest_file] + isfile(f) || touch(f) + end +end + +function assert_has_manifest(dir::String) + @assert isdir(dir) + if !isfile(dir |> project_file) + error("The given directory does not contain a Project.toml file -- it is not a package environment.") + end + if !isfile(dir |> manifest_file) + error("The given directory does not contain a Manifest.toml file. Use `Pkg.resolve` to generate a Manifest.toml from a Project.toml.") + end +end + + +function nb_and_dir_environments_equal(notebook::Notebook, dir::String) + try + ensure_has_nbpkg(notebook) + assert_has_manifest(dir) + true + catch + false + end && let + read(notebook |> project_file) == read(dir |> project_file) && + read(notebook |> manifest_file) == read(dir |> manifest_file) + end +end + + +function write_nb_to_dir(notebook::Notebook, dir::String) + ensure_has_nbpkg(notebook) + mkpath(dir) + + readwrite(notebook |> project_file, dir |> project_file) + readwrite(notebook |> manifest_file, dir |> manifest_file) +end + + +function write_dir_to_nb(dir::String, notebook::Notebook) + assert_has_manifest(dir) + + notebook.nbpkg_ctx = Pluto.PkgCompat.create_empty_ctx() + + readwrite(dir |> project_file, notebook |> project_file) + readwrite(dir |> manifest_file, notebook |> manifest_file) + + save_notebook(notebook) +end + +write_dir_to_nb(dir::String, notebook_path::String) = write_dir_to_nb(dir::String, load_notebook(notebook_path)) + +write_nb_to_dir(notebook_path::String, dir::String) = write_nb_to_dir(load_notebook(notebook_path), dir) +nb_and_dir_environments_equal(notebook_path::String, dir::String) = nb_and_dir_environments_equal(load_notebook(notebook_path), dir) + +""" +```julia +reset_notebook_environment(notebook_path::String; keep_project::Bool=false, backup::Bool=true) +``` + +Remove the embedded `Project.toml` and `Manifest.toml` from a notebook file, modifying the file. If `keep_project` is true, only `Manifest.toml` will be deleted. A backup file is created by default. +""" +function reset_notebook_environment(path::String; kwargs...) + Pluto.reset_nbpkg( + load_notebook_nobackup(path); + kwargs... + ) +end + +""" +```julia +reset_notebook_environment(notebook_path::String; backup::Bool=true, level::Pkg.UpgradeLevel=Pkg.UPLEVEL_MAJOR) +``` + +Update the embedded `Project.toml` and `Manifest.toml` in a notebook file, modifying the file. A [`Pkg.UpgradeLevel`](@ref) can be passed to the `level` keyword argument. A backup file is created by default. +""" +function update_notebook_environment(path::String; kwargs...) + Pluto.update_nbpkg( + Pluto.ServerSession(), + load_notebook_nobackup(path); + kwargs... + ) +end + + +function activate_notebook_environment(path::String) + notebook_ref = Ref(load_notebook(path)) + + ensure_has_nbpkg(notebook_ref[]) + + ourpath = joinpath(mktempdir(), basename(path)) + mkpath(ourpath) + + still_needed = Ref(true) + + save_token = Token() + + function maybe_update_nb() + withtoken(save_token) do + if still_needed[] + if !nb_and_dir_environments_equal(notebook_ref[], ourpath) + write_dir_to_nb(ourpath, notebook_ref[]) + println() + @info "Saved notebook package environment ✓" + println() + end + end + end + end + + function maybe_update_dir() + withtoken(save_token) do + if still_needed[] + if !nb_and_dir_environments_equal(notebook_ref[], ourpath) + write_nb_to_dir(notebook_ref[], ourpath) + println() + @info "New notebook package environment written to directory ✓" + println() + end + end + end + end + + maybe_update_dir() + + atexit(maybe_update_nb) + + Base.ACTIVE_PROJECT[] = ourpath + + # WATCH DIR PROJECT FILE + Pluto.@asynclog begin + while Base.ACTIVE_PROJECT[] == ourpath + FileWatching.watch_file(ourpath |> project_file) + # @warn "DIR PROJECT UPDATED" + sleep(.2) + + maybe_update_nb() + end + still_needed[] = false + end + + # WATCH DIR MANIFEST FILE + Pluto.@asynclog begin + while Base.ACTIVE_PROJECT[] == ourpath + FileWatching.watch_file(ourpath |> manifest_file) + # @warn "DIR MANIFEST UPDATED" + sleep(.5) + + maybe_update_nb() + end + still_needed[] = false + end + + # WATCH NOTEBOOK FILE + Pluto.@asynclog begin + while Base.ACTIVE_PROJECT[] == ourpath + FileWatching.watch_file(path) + # @warn "NOTEBOOK FILE UPDATED" + # we update the dir after a longer delay, because changes to the environment in the directory (written to the notebook) take precedence. + sleep(1) + + notebook_ref[] = load_notebook(path) + + maybe_update_dir() + end + still_needed[] = false + end + + # CHECK EVERY 5 SECONDS + # Pluto.@asynclog begin + # while still_needed[] + # sleep(5) + # maybe_update_nb() + # # we update the dir second, because changes to the environment in the directory (written to the notebook) take precedence. + # maybe_update_dir() + # end + # end + + println() + """ + + > Notebook environment activated! + + ## Step 1. + _Press `]` to open the Pkg REPL._ + + The notebook environment is currently active. + + ## Step 2. + The notebook file and your REPL environment are now synced. This means that: + 1. Any changes you make in the REPL environment will be written to the notebook file. For example, you can `pkg> update` or `pkg> add SomePackage`, and the notebook file will update. + 2. Whenever the notebook file changes, the REPL environment will be updated from the notebook file. + + ## Step 3. + When you are done, you can exit the notebook environment by deactivating it: + + ``` + pkg> activate + ``` + """ |> Markdown.parse |> display + println() + + +end + +const activate_notebook = activate_notebook_environment + +function testnb() + t = tempname() + + readwrite(Pluto.project_relative_path("test","packages","nb.jl"), t) + t +end + +end \ No newline at end of file diff --git a/src/webserver/Dynamic.jl b/src/webserver/Dynamic.jl index f608751460..40841ba480 100644 --- a/src/webserver/Dynamic.jl +++ b/src/webserver/Dynamic.jl @@ -2,6 +2,8 @@ import UUIDs: uuid1 import TableIOInterface: get_example_code, is_extension_supported +import .PkgCompat + "Will hold all 'response handlers': functions that respond to a WebSocket request from the client." const responses = Dict{Symbol,Function}() @@ -145,6 +147,19 @@ function notebook_to_js(notebook::Notebook) "is_first_value" => bondvalue.is_first_value ) for (key, bondvalue) in notebook.bonds), + "nbpkg" => let + ctx = notebook.nbpkg_ctx + Dict{String,Any}( + "enabled" => ctx !== nothing, + "restart_recommended_msg" => notebook.nbpkg_restart_recommended_msg, + "restart_required_msg" => notebook.nbpkg_restart_required_msg, + # TODO: cache this + "installed_versions" => ctx === nothing ? Dict{String,String}() : notebook.nbpkg_installed_versions_cache, + "terminal_outputs" => notebook.nbpkg_terminal_outputs, + "busy_packages" => notebook.nbpkg_busy_packages, + "instantiated" => notebook.nbpkg_ctx_instantiated, + ) + end, "cell_execution_order" => cell_id.(collect(topological_order(notebook))), ) end @@ -413,8 +428,9 @@ end without_initiator(🙋::ClientRequest) = ClientRequest(session=🙋.session, notebook=🙋.notebook) -responses[:restart_process] = function response_restrart_process(🙋::ClientRequest) +responses[:restart_process] = function response_restrart_process(🙋::ClientRequest; run_async::Bool=true) require_notebook(🙋) + if 🙋.notebook.process_status != ProcessStatus.waiting_to_restart 🙋.notebook.process_status = ProcessStatus.waiting_to_restart @@ -425,7 +441,7 @@ responses[:restart_process] = function response_restrart_process(🙋::ClientReq 🙋.notebook.process_status = ProcessStatus.starting send_notebook_changes!(🙋 |> without_initiator) - update_save_run!(🙋.session, 🙋.notebook, 🙋.notebook.cells; run_async=true, save=true) + update_save_run!(🙋.session, 🙋.notebook, 🙋.notebook.cells; run_async=run_async, save=true) end end @@ -496,6 +512,7 @@ function set_bond_values_reactive(; session::ServerSession, notebook::Notebook, end responses[:write_file] = function (🙋::ClientRequest) + require_notebook(🙋) path = 🙋.notebook.path reldir = "$(path |> basename).assets" dir = joinpath(path |> dirname, reldir) @@ -558,3 +575,24 @@ end""" code = missing end end + +responses[:nbpkg_available_versions] = function response_nbpkg_available_versions(🙋::ClientRequest) + # require_notebook(🙋) + all_versions = PkgCompat.package_versions(🙋.body["package_name"]) + putclientupdates!(🙋.session, 🙋.initiator, UpdateMessage(:🍕, Dict( + :versions => string.(all_versions), + ), nothing, nothing, 🙋.initiator)) +end + +responses[:package_completions] = function response_package_completions(🙋::ClientRequest) + results = PkgCompat.package_completions(🙋.body["query"]) + putclientupdates!(🙋.session, 🙋.initiator, UpdateMessage(:🍳, Dict( + :results => results, + ), nothing, nothing, 🙋.initiator)) +end + +responses[:pkg_update] = function response_pkg_update(🙋::ClientRequest) + require_notebook(🙋) + update_nbpkg(🙋.session, 🙋.notebook) + putclientupdates!(🙋.session, 🙋.initiator, UpdateMessage(:🦆, Dict(), nothing, nothing, 🙋.initiator)) +end diff --git a/src/webserver/MsgPack.jl b/src/webserver/MsgPack.jl index f595239388..1bbbad5cfb 100644 --- a/src/webserver/MsgPack.jl +++ b/src/webserver/MsgPack.jl @@ -5,12 +5,17 @@ import Dates import UUIDs: UUID import MsgPack import .Configuration +import Pkg # MsgPack.jl doesn't define a serialization method for MIME and UUID objects, so we write these ourselves: MsgPack.msgpack_type(::Type{<:MIME}) = MsgPack.StringType() MsgPack.msgpack_type(::Type{UUID}) = MsgPack.StringType() +MsgPack.msgpack_type(::Type{VersionNumber}) = MsgPack.StringType() +MsgPack.msgpack_type(::Type{Pkg.Types.VersionRange}) = MsgPack.StringType() MsgPack.to_msgpack(::MsgPack.StringType, m::MIME) = string(m) MsgPack.to_msgpack(::MsgPack.StringType, u::UUID) = string(u) +MsgPack.to_msgpack(::MsgPack.StringType, v::VersionNumber) = string(v) +MsgPack.to_msgpack(::MsgPack.StringType, v::Pkg.Types.VersionRange) = string(v) # Support for sending Dates MsgPack.msgpack_type(::Type{Dates.DateTime}) = MsgPack.ExtensionType() diff --git a/src/webserver/REPLTools.jl b/src/webserver/REPLTools.jl index 1d81735299..29ad1b3817 100644 --- a/src/webserver/REPLTools.jl +++ b/src/webserver/REPLTools.jl @@ -1,5 +1,6 @@ import FuzzyCompletions: complete_path, completion_text, score import Distributed +import .PkgCompat: package_completions using Markdown ### @@ -57,6 +58,11 @@ responses[:completepath] = function response_completepath(🙋::ClientRequest) putclientupdates!(🙋.session, 🙋.initiator, msg) end +function package_name_to_complete(str) + matches = match(r"(import|using) ([a-zA-Z0-9]+)$", str) + matches === nothing ? nothing : matches[2] +end + responses[:complete] = function response_complete(🙋::ClientRequest) try require_notebook(🙋) catch; return; end query = 🙋.body["query"] @@ -64,16 +70,22 @@ responses[:complete] = function response_complete(🙋::ClientRequest) workspace = WorkspaceManager.get_workspace((🙋.session, 🙋.notebook)) - results_text, loc, found = if will_run_code(🙋.notebook) && isready(workspace.dowork_token) - # we don't use eval_format_fetch_in_workspace because we don't want the output to be string-formatted. - # This works in this particular case, because the return object, a `Completion`, exists in this scope too. - Distributed.remotecall_eval(Main, workspace.pid, :(PlutoRunner.completion_fetcher( - $query, $pos, - getfield(Main, $(QuoteNode(workspace.module_name))), - ))) + results_text, loc, found = if package_name_to_complete(query) !== nothing + p = package_name_to_complete(query) + cs = package_completions(p) |> sort + [(c,"package",true) for c in cs], (nextind(query, pos-length(p)):pos), true else - # We can at least autocomplete general julia things: - PlutoRunner.completion_fetcher(query, pos, Main) + if will_run_code(🙋.notebook) && isready(workspace.dowork_token) + # we don't use eval_format_fetch_in_workspace because we don't want the output to be string-formatted. + # This works in this particular case, because the return object, a `Completion`, exists in this scope too. + Distributed.remotecall_eval(Main, workspace.pid, :(PlutoRunner.completion_fetcher( + $query, $pos, + getfield(Main, $(QuoteNode(workspace.module_name))), + ))) + else + # We can at least autocomplete general julia things: + PlutoRunner.completion_fetcher(query, pos, Main) + end end start_utf8 = loc.start diff --git a/src/webserver/SessionActions.jl b/src/webserver/SessionActions.jl index 6efc1ae3bf..2b721e8a30 100644 --- a/src/webserver/SessionActions.jl +++ b/src/webserver/SessionActions.jl @@ -1,6 +1,6 @@ module SessionActions -import ..Pluto: ServerSession, Notebook, emptynotebook, tamepath, new_notebooks_directory, without_pluto_file_extension, numbered_until_new, readwrite, update_save_run!, putnotebookupdates!, putplutoupdates!, load_notebook, clientupdate_notebook_list, WorkspaceManager, @asynclog +import ..Pluto: ServerSession, Notebook, Cell, emptynotebook, tamepath, new_notebooks_directory, without_pluto_file_extension, numbered_until_new, readwrite, update_save_run!, putnotebookupdates!, putplutoupdates!, load_notebook, clientupdate_notebook_list, WorkspaceManager, @asynclog struct NotebookIsRunningException <: Exception notebook::Notebook @@ -68,7 +68,37 @@ function save_upload(content::Vector{UInt8}) end function new(session::ServerSession; run_async=true) - nb = emptynotebook() + nb = if session.options.server.init_with_file_viewer + + file_viewer_code = """html\"\"\" + + + + \"\"\" + """ + Notebook([Cell(), Cell(code=file_viewer_code, code_folded=true)]) + + else + emptynotebook() + end update_save_run!(session, nb, nb.cells; run_async=run_async, prerender_text=true) session.notebooks[nb.notebook_id] = nb @@ -82,6 +112,9 @@ function new(session::ServerSession; run_async=true) end function shutdown(session::ServerSession, notebook::Notebook; keep_in_session=false, async=false) + notebook.nbpkg_restart_recommended_msg = nothing + notebook.nbpkg_restart_required_msg = nothing + if !keep_in_session listeners = putnotebookupdates!(session, notebook) # TODO: shutdown message delete!(session.notebooks, notebook.notebook_id) diff --git a/src/webserver/Static.jl b/src/webserver/Static.jl index de98fac69b..a697c3a50e 100644 --- a/src/webserver/Static.jl +++ b/src/webserver/Static.jl @@ -244,9 +244,9 @@ function http_router_for(session::ServerSession) ) do request::HTTP.Request try notebook = notebook_from_uri(request) - response = HTTP.Response(200, generate_html(notebook)) + response = HTTP.Response(200, generate_html(notebook; pluto_cdn_root="./")) push!(response.headers, "Content-Type" => "text/html; charset=utf-8") - push!(response.headers, "Content-Disposition" => "inline; filename=\"$(basename(notebook.path)).html\"") + # push!(response.headers, "Content-Disposition" => "inline; filename=\"$(basename(notebook.path)).html\"") response catch e return error_response(400, "Bad query", "Please report this error!", sprint(showerror, e, stacktrace(catch_backtrace()))) diff --git a/test/ExpressionExplorer.jl b/test/ExpressionExplorer.jl index e493565b28..efc9be34ae 100644 --- a/test/ExpressionExplorer.jl +++ b/test/ExpressionExplorer.jl @@ -454,7 +454,6 @@ Some of these @test_broken lines are commented out to prevent printing to the te @test testee(:(ex = :(yayo + $r)), [], [:ex], [], []) # @test_broken testee(:(ex = :(yayo + $r)), [:r], [:ex], [], [], verbose=false) end - @testset "Extracting `using` and `import`" begin expr = quote using A diff --git a/test/Notebook.jl b/test/Notebook.jl index b6d47cbaf5..cbe2ac3115 100644 --- a/test/Notebook.jl +++ b/test/Notebook.jl @@ -1,6 +1,7 @@ using Test -import Pluto: Notebook, ServerSession, ClientSession, Cell, load_notebook, load_notebook_nobackup, save_notebook, WorkspaceManager, cutename, numbered_until_new +import Pluto: Notebook, ServerSession, ClientSession, Cell, load_notebook, load_notebook_nobackup, save_notebook, WorkspaceManager, cutename, numbered_until_new, readwrite import Random +import Pkg # We define some notebooks explicitly, and not as a .jl notebook file, to avoid circular reasoning 🤔 function basic_notebook() @@ -24,7 +25,7 @@ function basic_notebook() """), # test included InteractiveUtils import Cell("subtypes(Number)"), - ]) + ]) |> init_packages! end function shuffled_notebook() @@ -37,7 +38,7 @@ function shuffled_notebook() Cell("t = 1"), Cell("w = v"), Cell("u = t"), - ]) + ]) |> init_packages! end function shuffled_with_imports_notebook() @@ -60,7 +61,13 @@ function shuffled_with_imports_notebook() x using REPL end"""), - ]) + ]) |> init_packages! +end + +function init_packages!(nb::Notebook) + nb.topology = Pluto.updated_topology(nb.topology, nb, nb.cells) + Pluto.sync_nbpkg_core(nb) + return nb end function bad_code_notebook() @@ -71,7 +78,7 @@ function bad_code_notebook() [[["""), Cell("using Aasdfdsf"), - ]) + ]) |> init_packages! end function bonds_notebook() @@ -87,11 +94,19 @@ function bonds_notebook() Cell("w = Wow(10)"), Cell("@bind z w"), Cell("@assert z == 10"), - ]) + ]) |> init_packages! +end + + +function project_notebook() + Notebook([ + Cell("using Dates"), + Cell("using Example"), + ]) |> init_packages! end @testset "Notebook Files" begin - nbs = [String(nameof(f)) => f() for f in [basic_notebook, shuffled_notebook, shuffled_with_imports_notebook, bad_code_notebook, bonds_notebook]] + nbs = [String(nameof(f)) => f() for f in [basic_notebook, shuffled_notebook, shuffled_with_imports_notebook, bad_code_notebook, bonds_notebook, project_notebook]] @testset "Sample notebooks " begin # Also adds them to the `nbs` list @@ -100,6 +115,7 @@ end @testset "$(file)" begin nb = @test_nowarn load_notebook_nobackup(path) + @test length(nb.cells) > 0 push!(nbs, "sample " * file => nb) end end @@ -117,11 +133,10 @@ end @testset "I/O basic" begin @testset "$(name)" for (name, nb) in nbs - @test let - save_notebook(nb) - result = load_notebook_nobackup(nb.path) - notebook_inputs_equal(nb, result) - end + save_notebook(nb) + # @info "File" name Text(read(nb.path,String)) + result = load_notebook_nobackup(nb.path) + @test notebook_inputs_equal(nb, result) end end @@ -142,8 +157,33 @@ end end end + @testset "Bijection test" begin + @testset "$(name)" for (name, nb) in nbs + new_path = tempname() + @assert !isfile(new_path) + readwrite(nb.path, new_path) + + # load_notebook also does parsing and analysis - this is needed to save the notebook with cells in their correct order + # laod_notebook is how they are normally loaded, load_notebook_nobackup + new_nb = load_notebook(new_path) + + before_contents = read(new_path, String) + + after_path = tempname() + write(after_path, before_contents) + + after = load_notebook(after_path) + after_contents = read(after_path, String) + + if name != String(nameof(bad_code_notebook)) + @test Text(before_contents) == Text(after_contents) + end + end + end + # Some notebooks are designed to error (inside/outside Pluto) - expect_error = [String(nameof(bad_code_notebook)), "sample Interactivity.jl"] + expect_error = [String(nameof(bad_code_notebook)), String(nameof(project_notebook)), "sample Interactivity.jl"] + @testset "Runnable without Pluto" begin @testset "$(name)" for (name, nb) in nbs @@ -151,8 +191,6 @@ end @assert !isfile(new_path) cp(nb.path, new_path) - # load_notebook also does parsing and analysis - this is needed to save the notebook with cells in their correct order - # laod_notebook is how they are normally loaded, load_notebook_nobackup new_nb = load_notebook(new_path) # println(read(new_nb.path, String)) @@ -199,20 +237,23 @@ end @test num_backups_in(new_dir) == 0 # Delete last line - cp(nb.path, new_path, force=true) - to_write = readlines(new_path)[1:end - 1] - write(new_path, join(to_write, '\n')) - @test_logs (:warn, r"Backup saved to") load_notebook(new_path) - @test num_backups_in(new_dir) == 1 + # cp(nb.path, new_path, force=true) + # to_write = readlines(new_path)[1:end - 1] + # write(new_path, join(to_write, '\n')) + # @test_logs (:warn, r"Backup saved to") load_notebook(new_path) + # @test num_backups_in(new_dir) == 1 - # Add a line + # Duplicate first line cp(nb.path, new_path, force=true) - to_write = push!(readlines(new_path), "1+1") + to_write = let + old_lines = readlines(new_path) + [old_lines[1], old_lines...] + end write(new_path, join(to_write, '\n')) @test_logs (:warn, r"Backup saved to") load_notebook(new_path) - @test num_backups_in(new_dir) == 2 + @test num_backups_in(new_dir) == 1 - @test readdir(new_dir) == ["nb backup 1.jl", "nb backup 2.jl", "nb.jl"] + @test readdir(new_dir) == ["nb backup 1.jl", "nb.jl"] end end diff --git a/test/WorkspaceManager.jl b/test/WorkspaceManager.jl index b975797aa1..707d07a837 100644 --- a/test/WorkspaceManager.jl +++ b/test/WorkspaceManager.jl @@ -81,7 +81,7 @@ import Distributed project_relative_path("Project.toml") end - @testset "Pluto inside Pluto" begin + Sys.iswindows() || (VERSION < v"1.6.0-a") || @testset "Pluto inside Pluto" begin client = ClientSession(:fakeA, nothing) 🍭 = ServerSession() diff --git a/test/helpers.jl b/test/helpers.jl index 7b2e817d31..9a69f9a56c 100644 --- a/test/helpers.jl +++ b/test/helpers.jl @@ -1,6 +1,6 @@ import Pluto import Pluto.ExpressionExplorer -import Pluto.ExpressionExplorer: SymbolsState, compute_symbolreferences, FunctionNameSignaturePair +import Pluto.ExpressionExplorer: SymbolsState, compute_symbolreferences, FunctionNameSignaturePair, UsingsImports, compute_usings_imports using Test function Base.show(io::IO, s::SymbolsState) @@ -170,3 +170,9 @@ function num_backups_in(dir::AbstractString) occursin("backup", fn) end end + +has_embedded_pkgfiles(contents::AbstractString) = + occursin("PROJECT", contents) && occursin("MANIFEST", contents) + +has_embedded_pkgfiles(nb::Pluto.Notebook) = + read(nb.path, String) |> has_embedded_pkgfiles \ No newline at end of file diff --git a/test/packages/Basic.jl b/test/packages/Basic.jl new file mode 100644 index 0000000000..3c8aa60e9e --- /dev/null +++ b/test/packages/Basic.jl @@ -0,0 +1,525 @@ +# using LibGit2 +import Pkg +using Test +using Pluto.Configuration: CompilerOptions +using Pluto.WorkspaceManager: _merge_notebook_compiler_options +import Pluto: update_save_run!, update_run!, WorkspaceManager, ClientSession, ServerSession, Notebook, Cell, project_relative_path, SessionActions, load_notebook +import Pluto.PkgUtils +import Pluto.PkgCompat +import Distributed + +const pluto_test_registry_spec = Pkg.RegistrySpec(; + url="https://github.com/JuliaPluto/PlutoPkgTestRegistry", + uuid=Base.UUID("96d04d5f-8721-475f-89c4-5ee455d3eda0"), + name="PlutoPkgTestRegistry", +) + +@testset "Built-in Pkg" begin + + # Pkg.Registry.rm("General") + Pkg.Registry.add(pluto_test_registry_spec) + + + @testset "Basic" begin + fakeclient = ClientSession(:fake, nothing) + 🍭 = ServerSession() + 🍭.connected_clients[fakeclient.id] = fakeclient + + # See https://github.com/JuliaPluto/PlutoPkgTestRegistry + + notebook = Notebook([ + Cell("import PlutoPkgTestA"), # cell 1 + Cell("PlutoPkgTestA.MY_VERSION |> Text"), + Cell("import PlutoPkgTestB"), # cell 3 + Cell("PlutoPkgTestB.MY_VERSION |> Text"), + Cell("import PlutoPkgTestC"), # cell 5 + Cell("PlutoPkgTestC.MY_VERSION |> Text"), + Cell("import PlutoPkgTestD"), # cell 7 + Cell("PlutoPkgTestD.MY_VERSION |> Text"), + Cell("import Dates"), + # eval to hide the import from Pluto's analysis + Cell("eval(:(import DataFrames))") + ]) + fakeclient.connected_notebook = notebook + + @test !notebook.nbpkg_ctx_instantiated + + update_save_run!(🍭, notebook, notebook.cells[[1, 2, 7, 8]]) # import A and D + @test notebook.cells[1].errored == false + @test notebook.cells[2].errored == false + @test notebook.cells[7].errored == false + @test notebook.cells[8].errored == false + + @test notebook.nbpkg_ctx !== nothing + @test notebook.nbpkg_restart_recommended_msg === nothing + @test notebook.nbpkg_restart_required_msg === nothing + @test notebook.nbpkg_ctx_instantiated + @test notebook.nbpkg_busy_packages |> isempty + + terminals = notebook.nbpkg_terminal_outputs + + @test haskey(terminals, "PlutoPkgTestA") + @test haskey(terminals, "PlutoPkgTestD") + # they were installed in one batch, so their terminal outputs should be the same + @test terminals["PlutoPkgTestA"] == terminals["PlutoPkgTestD"] + + + @test notebook.cells[2].output.body == "0.3.1" # A + @test notebook.cells[8].output.body == "0.1.0" # D + + @test PkgCompat.get_manifest_version(notebook.nbpkg_ctx, "PlutoPkgTestA") == v"0.3.1" + @test PkgCompat.get_manifest_version(notebook.nbpkg_ctx, "PlutoPkgTestD") == v"0.1.0" + + + old_A_terminal = deepcopy(terminals["PlutoPkgTestA"]) + @show old_A_terminal + + update_save_run!(🍭, notebook, notebook.cells[[3, 4]]) # import B + + @test notebook.cells[3].errored == false + @test notebook.cells[4].errored == false + + @test notebook.nbpkg_ctx !== nothing + @test notebook.nbpkg_restart_recommended_msg === nothing + @test notebook.nbpkg_restart_required_msg === nothing + + @test haskey(terminals, "PlutoPkgTestB") + @test terminals["PlutoPkgTestA"] == terminals["PlutoPkgTestD"] == old_A_terminal + + @test terminals["PlutoPkgTestA"] != terminals["PlutoPkgTestB"] + + + @test notebook.cells[4].output.body == "1.0.0" # B + + # running the 5th cell will import PlutoPkgTestC, putting a 0.2 compatibility bound on PlutoPkgTestA. This means that a notebook restart is required, since PlutoPkgTestA was already loaded at version 0.3.1. + update_save_run!(🍭, notebook, notebook.cells[[5, 6]]) + + @test notebook.cells[5].errored == false + @test notebook.cells[6].errored == false + + @test notebook.nbpkg_ctx !== nothing + @test ( + notebook.nbpkg_restart_recommended_msg !== nothing || notebook.nbpkg_restart_required_msg !== nothing + ) + @test notebook.nbpkg_restart_required_msg !== nothing + + # running cells again should persist the restart message + + update_save_run!(🍭, notebook, notebook.cells[1:8]) + @test notebook.nbpkg_restart_required_msg !== nothing + + Pluto.response_restrart_process(Pluto.ClientRequest( + session=🍭, + notebook=notebook, + ); run_async=false) + + # @test_nowarn SessionActions.shutdown(🍭, notebook; keep_in_session=true, async=true) + # @test_nowarn update_save_run!(🍭, notebook, notebook.cells[1:8]; , save=true) + + @test notebook.cells[1].errored == false + @test notebook.cells[2].errored == false + @test notebook.cells[3].errored == false + @test notebook.cells[4].errored == false + @test notebook.cells[5].errored == false + @test notebook.cells[6].errored == false + @test notebook.cells[7].errored == false + @test notebook.cells[8].errored == false + + @test notebook.nbpkg_ctx !== nothing + @test notebook.nbpkg_restart_recommended_msg === nothing + @test notebook.nbpkg_restart_required_msg === nothing + + + @test notebook.cells[2].output.body == "0.2.2" + @test notebook.cells[4].output.body == "1.0.0" + @test notebook.cells[6].output.body == "1.0.0" + @test notebook.cells[8].output.body == "0.1.0" + + + update_save_run!(🍭, notebook, notebook.cells[9]) + + @test notebook.cells[9].errored == false + @test notebook.nbpkg_ctx !== nothing + @test notebook.nbpkg_restart_recommended_msg === nothing + @test notebook.nbpkg_restart_required_msg === nothing + + + # we should have an isolated environment, so importing DataFrames should not work, even though it is available in the parent process. + update_save_run!(🍭, notebook, notebook.cells[10]) + @test notebook.cells[10].errored == true + + + ptoml_file() = PkgCompat.project_file(notebook) + mtoml_file() = PkgCompat.manifest_file(notebook) + + ptoml_contents() = read(ptoml_file(), String) + mtoml_contents() = read(mtoml_file(), String) + + nb_contents() = read(notebook.path, String) + + @testset "Project & Manifest stored in notebook" begin + + @test occursin(ptoml_contents(), nb_contents()) + @test occursin(mtoml_contents(), nb_contents()) + + @test occursin("PlutoPkgTestA", mtoml_contents()) + @test occursin("PlutoPkgTestB", mtoml_contents()) + @test occursin("PlutoPkgTestC", mtoml_contents()) + @test occursin("PlutoPkgTestD", mtoml_contents()) + @test occursin("Dates", mtoml_contents()) + @test count("PlutoPkgTestA", ptoml_contents()) == 2 # once in [deps], once in [compat] + @test count("PlutoPkgTestB", ptoml_contents()) == 2 + @test count("PlutoPkgTestC", ptoml_contents()) == 2 + @test count("PlutoPkgTestD", ptoml_contents()) == 2 + @test count("Dates", ptoml_contents()) == 1 # once in [deps], but not in [compat] because it is a stdlib + + ptoml = Pkg.TOML.parse(ptoml_contents()) + + @test haskey(ptoml["compat"], "PlutoPkgTestA") + @test haskey(ptoml["compat"], "PlutoPkgTestB") + @test haskey(ptoml["compat"], "PlutoPkgTestC") + @test haskey(ptoml["compat"], "PlutoPkgTestD") + @test !haskey(ptoml["compat"], "Dates") + end + + ## remove `import Dates` + setcode(notebook.cells[9], "") + update_save_run!(🍭, notebook, notebook.cells[9]) + + # removing a stdlib does not require a restart + @test notebook.cells[9].errored == false + @test notebook.nbpkg_ctx !== nothing + @test notebook.nbpkg_restart_recommended_msg === nothing + @test notebook.nbpkg_restart_required_msg === nothing + + @test count("Dates", ptoml_contents()) == 0 + + + ## remove `import PlutoPkgTestD` + setcode(notebook.cells[7], "") + update_save_run!(🍭, notebook, notebook.cells[7]) + + @test notebook.cells[7].errored == false + @test notebook.nbpkg_ctx !== nothing + @test notebook.nbpkg_restart_recommended_msg !== nothing # recommend restart + @test notebook.nbpkg_restart_required_msg === nothing + + @test count("PlutoPkgTestD", ptoml_contents()) == 0 + + + WorkspaceManager.unmake_workspace((🍭, notebook)) + end + + simple_import_path = joinpath(@__DIR__, "simple_import.jl") + simple_import_notebook = read(simple_import_path, String) + + @testset "Manifest loading" begin + fakeclient = ClientSession(:fake, nothing) + 🍭 = ServerSession() + 🍭.connected_clients[fakeclient.id] = fakeclient + + dir = mktempdir() + path = joinpath(dir, "hello.jl") + write(path, simple_import_notebook) + + notebook = SessionActions.open(🍭, path; run_async=false) + fakeclient.connected_notebook = notebook + + @test num_backups_in(dir) == 0 + + + @test notebook.nbpkg_ctx !== nothing + @test notebook.nbpkg_restart_recommended_msg === nothing + @test notebook.nbpkg_restart_required_msg === nothing + + @test notebook.cells[1].errored == false + @test notebook.cells[2].errored == false + + @test notebook.cells[2].output.body == "0.2.2" + + WorkspaceManager.unmake_workspace((🍭, notebook)) + end + + + @testset "Pkg cell" begin + fakeclient = ClientSession(:fake, nothing) + 🍭 = ServerSession() + 🍭.connected_clients[fakeclient.id] = fakeclient + + + notebook = Notebook([ + Cell("1"), + Cell("2"), + Cell("3"), + Cell("4"), + Cell("5"), + Cell("6"), + ]) + fakeclient.connected_notebook = notebook + + update_save_run!(🍭, notebook, notebook.cells) + + # not necessary since there are no packages: + # @test has_embedded_pkgfiles(notebook) + + setcode(notebook.cells[1], "import Pkg") + update_save_run!(🍭, notebook, notebook.cells[1]) + setcode(notebook.cells[2], "Pkg.activate(mktempdir())") + update_save_run!(🍭, notebook, notebook.cells[2]) + + @test notebook.cells[1].errored == false + @test notebook.cells[2].errored == false + @test notebook.nbpkg_ctx === nothing + @test notebook.nbpkg_restart_recommended_msg === nothing + @test notebook.nbpkg_restart_required_msg === nothing + @test !has_embedded_pkgfiles(notebook) + + setcode(notebook.cells[3], "Pkg.add(\"JSON\")") + update_save_run!(🍭, notebook, notebook.cells[3]) + setcode(notebook.cells[4], "using JSON") + update_save_run!(🍭, notebook, notebook.cells[4]) + setcode(notebook.cells[5], "using Dates") + update_save_run!(🍭, notebook, notebook.cells[5]) + + @test notebook.cells[3].errored == false + @test notebook.cells[4].errored == false + @test notebook.cells[5].errored == false + + @test !has_embedded_pkgfiles(notebook) + + setcode(notebook.cells[2], "2") + setcode(notebook.cells[3], "3") + update_save_run!(🍭, notebook, notebook.cells[2:3]) + + @test notebook.nbpkg_ctx !== nothing + @test notebook.nbpkg_restart_required_msg !== nothing + @test has_embedded_pkgfiles(notebook) + + WorkspaceManager.unmake_workspace((🍭, notebook)) + end + + pre_pkg_notebook = read(joinpath(@__DIR__, "old_import.jl"), String) + + local post_pkg_notebook = nothing + + @testset "Backwards compat" begin + fakeclient = ClientSession(:fake, nothing) + 🍭 = ServerSession() + 🍭.connected_clients[fakeclient.id] = fakeclient + + dir = mktempdir() + path = joinpath(dir, "hello.jl") + write(path, pre_pkg_notebook) + + @test num_backups_in(dir) == 0 + + notebook = SessionActions.open(🍭, path; run_async=false) + fakeclient.connected_notebook = notebook + nb_contents() = read(notebook.path, String) + + @test num_backups_in(dir) == 0 + # @test num_backups_in(dir) == 1 + + post_pkg_notebook = read(path, String) + + # test that pkg cells got added + @test length(post_pkg_notebook) > length(pre_pkg_notebook) + 50 + @test has_embedded_pkgfiles(notebook) + + @test notebook.nbpkg_ctx !== nothing + @test notebook.nbpkg_restart_recommended_msg === nothing + @test notebook.nbpkg_restart_required_msg === nothing + + WorkspaceManager.unmake_workspace((🍭, notebook)) + end + + @testset "Forwards compat" begin + # Using Distributed, we will create a new Julia process in which we install Pluto 0.14.7 (before PlutoPkg). We run the new notebook file on the old Pluto. + p = Distributed.addprocs(1) |> first + + @test post_pkg_notebook isa String + + Distributed.remotecall_eval(Main, p, quote + path = tempname() + write(path, $(post_pkg_notebook)) + import Pkg + # optimization: + if isdefined(Pkg, :UPDATED_REGISTRY_THIS_SESSION) + Pkg.UPDATED_REGISTRY_THIS_SESSION[] = true + end + + Pkg.activate(mktempdir()) + Pkg.add(Pkg.PackageSpec(;name="Pluto",version=v"0.14.7")) + import Pluto + @assert Pluto.PLUTO_VERSION == v"0.14.7" + + s = Pluto.ServerSession() + s.options.evaluation.workspace_use_distributed = false + + nb = Pluto.SessionActions.open(s, path; run_async=false) + + nothing + end) + + # Cells that use Example will error because the package is not installed. + + # @test Distributed.remotecall_eval(Main, p, quote + # nb.cells[1].errored == false + # end) + @test Distributed.remotecall_eval(Main, p, quote + nb.cells[2].errored == false + end) + # @test Distributed.remotecall_eval(Main, p, quote + # nb.cells[3].errored == false + # end) + # @test Distributed.remotecall_eval(Main, p, quote + # nb.cells[3].output.body == "25" + # end) + + Distributed.rmprocs([p]) + end + + @testset "PkgUtils -- reset" begin + dir = mktempdir() + f = joinpath(dir, "hello.jl") + + write(f, simple_import_notebook) + + @test num_backups_in(dir) == 0 + Pluto.PkgUtils.reset_notebook_environment(f) + + @test num_backups_in(dir) == 1 + @test !has_embedded_pkgfiles(read(f, String)) + end + + @testset "PkgUtils -- update" begin + dir = mktempdir() + f = joinpath(dir, "hello.jl") + + write(f, simple_import_notebook) + @test !occursin("0.3.1", read(f, String)) + + @test num_backups_in(dir) == 0 + Pluto.PkgUtils.update_notebook_environment(f) + + @test num_backups_in(dir) == 1 + @test has_embedded_pkgfiles(read(f, String)) + @test !Pluto.only_versions_differ(f, simple_import_path) + @test occursin("0.3.1", read(f, String)) + + Pluto.PkgUtils.update_notebook_environment(f) + @test_skip num_backups_in(dir) == 1 + end + + @testset "Bad files" begin + @testset "$(name)" for name in ["corrupted_manifest", "unregistered_import"] + + original_path = joinpath(@__DIR__, "$(name).jl") + original_contents = read(original_path, String) + + fakeclient = ClientSession(:fake, nothing) + 🍭 = ServerSession() + 🍭.connected_clients[fakeclient.id] = fakeclient + + dir = mktempdir() + path = joinpath(dir, "hello.jl") + write(path, original_contents) + + @test num_backups_in(dir) == 0 + + notebook = SessionActions.open(🍭, path; run_async=false) + fakeclient.connected_notebook = notebook + nb_contents() = read(notebook.path, String) + + should_restart = ( + notebook.nbpkg_restart_recommended_msg !== nothing || notebook.nbpkg_restart_required_msg !== nothing + ) + + # if name == "corrupted_manifest" + # @test !should_restart + # end + + # this breaks julia for somee reason: + # # we don't want to recommend restart right after launch, but it's easier for us + # @test_broken !should_restart + # end + + if should_restart + Pluto.response_restrart_process(Pluto.ClientRequest( + session=🍭, + notebook=notebook, + ); run_async=false) + end + + if name != "unregistered_import" + @test notebook.cells[1].errored == false + @test notebook.cells[2].errored == false + @test notebook.cells[2].output.body == "0.2.2" # the Project.toml remained, so we did not lose our compat bound. + @test has_embedded_pkgfiles(notebook) + end + + + @test !Pluto.only_versions_differ(notebook.path, original_path) + + @test notebook.nbpkg_ctx !== nothing + @test notebook.nbpkg_restart_recommended_msg === nothing + @test notebook.nbpkg_restart_required_msg === nothing + + setcode(notebook.cells[2], "1 + 1") + update_save_run!(🍭, notebook, notebook.cells[2]) + @test notebook.cells[2].output.body == "2" + + + setcode(notebook.cells[2], """ + begin + import PlutoPkgTestD + PlutoPkgTestD.MY_VERSION |> Text + end + """) + update_save_run!(🍭, notebook, notebook.cells[2]) + @test notebook.cells[2].output.body == "0.1.0" + + @test has_embedded_pkgfiles(notebook) + + WorkspaceManager.unmake_workspace((🍭, notebook)) + end + + end + + # @test false + + # @testset "File format" begin + # notebook = Notebook([ + # Cell("import PlutoPkgTestA"), # cell 1 + # Cell("PlutoPkgTestA.MY_VERSION |> Text"), + # Cell("import PlutoPkgTestB"), # cell 3 + # Cell("PlutoPkgTestB.MY_VERSION |> Text"), + # Cell("import PlutoPkgTestC"), # cell 5 + # Cell("PlutoPkgTestC.MY_VERSION |> Text"), + # Cell("import PlutoPkgTestD"), # cell 7 + # Cell("PlutoPkgTestD.MY_VERSION |> Text"), + # Cell("import PlutoPkgTestE"), # cell 9 + # Cell("PlutoPkgTestE.MY_VERSION |> Text"), + # Cell("eval(:(import DataFrames))") + # ]) + + # file1 = tempname() + # notebook.path = file1 + + # save_notebook() + + + # save_notebook + # end + + + Pkg.Registry.rm(pluto_test_registry_spec) + # Pkg.Registry.add("General") +end + +# reg_path = mktempdir() +# repo = LibGit2.clone("https://github.com/JuliaRegistries/General.git", reg_path) + +# LibGit2.checkout!(repo, "aef26d37e1d0e8f8387c011ccb7c4a38398a18f6") + + diff --git a/test/packages/PkgCompat.jl b/test/packages/PkgCompat.jl new file mode 100644 index 0000000000..4979e09a86 --- /dev/null +++ b/test/packages/PkgCompat.jl @@ -0,0 +1,122 @@ +import Pluto.PkgCompat +import Pluto +import Pluto: update_save_run!, update_run!, WorkspaceManager, ClientSession, ServerSession, Notebook, Cell, project_relative_path, SessionActions, load_notebook +using Test +import Pkg + + +@testset "PkgCompat" begin + + @testset "Available versions" begin + vs = PkgCompat.package_versions("HTTP") + + @test v"0.9.0" ∈ vs + @test v"0.9.1" ∈ vs + @test "stdlib" ∉ vs + @test PkgCompat.package_exists("HTTP") + + vs = PkgCompat.package_versions("Dates") + + @test vs == ["stdlib"] + @test PkgCompat.package_exists("Dates") + + @test PkgCompat.is_stdlib("Dates") + @test !PkgCompat.is_stdlib("PlutoUI") + + + vs = PkgCompat.package_versions("Dateskjashdfkjahsdfkjh") + + @test isempty(vs) + @test !PkgCompat.package_exists("Dateskjashdfkjahsdfkjh") + end + + @testset "Installed versions" begin + # we are querying the package environment that is currently active for testing + ctx = Pkg.Types.Context() + + ctx = PkgCompat.create_empty_ctx() + Pkg.add(ctx, [Pkg.PackageSpec("HTTP"), Pkg.PackageSpec("UUIDs"), ]) + @test PkgCompat.get_manifest_version(ctx, "HTTP") > v"0.8.0" + @test PkgCompat.get_manifest_version(ctx, "UUIDs") == "stdlib" + + end + + @testset "Completions" begin + cs = PkgCompat.package_completions("Hyper") + @test "HypertextLiteral" ∈ cs + @test "Hyperscript" ∈ cs + + cs = PkgCompat.package_completions("Date") + @test "Dates" ∈ cs + + cs = PkgCompat.package_completions("Dateskjashdfkjahsdfkjh") + + @test isempty(cs) + end + + @testset "Compat manipulation" begin + old_path = joinpath(@__DIR__, "old_artifacts_import.jl") + old_contents = read(old_path, String) + + dir = mktempdir() + path = joinpath(dir, "hello.jl") + + write(path, old_contents) + + notebook = load_notebook(path) + ptoml_file() = PkgCompat.project_file(notebook) + mtoml_file() = PkgCompat.manifest_file(notebook) + ptoml_contents() = read(ptoml_file(), String) + mtoml_contents() = read(mtoml_file(), String) + + @test num_backups_in(dir) == 0 + + + + @test Pluto.only_versions_or_lineorder_differ(old_path, path) + + ptoml = Pkg.TOML.parse(ptoml_contents()) + @test haskey(ptoml["deps"], "PlutoPkgTestA") + @test haskey(ptoml["deps"], "Artifacts") + @test haskey(ptoml["compat"], "PlutoPkgTestA") + @test haskey(ptoml["compat"], "Artifacts") + + notebook.nbpkg_ctx = PkgCompat.clear_stdlib_compat_entries(notebook.nbpkg_ctx) + + ptoml = Pkg.TOML.parse(ptoml_contents()) + @test haskey(ptoml["deps"], "PlutoPkgTestA") + @test haskey(ptoml["deps"], "Artifacts") + @test haskey(ptoml["compat"], "PlutoPkgTestA") + if PkgCompat.is_stdlib("Artifacts") + @test !haskey(ptoml["compat"], "Artifacts") + end + + old_a_compat_entry = ptoml["compat"]["PlutoPkgTestA"] + notebook.nbpkg_ctx = PkgCompat.clear_auto_compat_entries(notebook.nbpkg_ctx) + + ptoml = Pkg.TOML.parse(ptoml_contents()) + @test haskey(ptoml["deps"], "PlutoPkgTestA") + @test haskey(ptoml["deps"], "Artifacts") + @test !haskey(ptoml, "compat") + compat = get(ptoml, "compat", Dict()) + @test !haskey(compat, "PlutoPkgTestA") + @test !haskey(compat, "Artifacts") + + notebook.nbpkg_ctx = PkgCompat.write_auto_compat_entries(notebook.nbpkg_ctx) + + ptoml = Pkg.TOML.parse(ptoml_contents()) + @test haskey(ptoml["deps"], "PlutoPkgTestA") + @test haskey(ptoml["deps"], "Artifacts") + @test haskey(ptoml["compat"], "PlutoPkgTestA") + if PkgCompat.is_stdlib("Artifacts") + @test !haskey(ptoml["compat"], "Artifacts") + end + + + end + + + @testset "Misc" begin + PkgCompat.create_empty_ctx() + end +end diff --git a/test/packages/corrupted_manifest.jl b/test/packages/corrupted_manifest.jl new file mode 100644 index 0000000000..892b975104 --- /dev/null +++ b/test/packages/corrupted_manifest.jl @@ -0,0 +1,40 @@ +### A Pluto.jl notebook ### +# v0.15.0 + +using Markdown +using InteractiveUtils + +# ╔═╡ 1bb0c024-c7a9-11eb-216f-839261af9684 +import PlutoPkgTestA + +# ╔═╡ 12fc51ea-0989-4bb2-a3a1-f1cc3bf2992d +PlutoPkgTestA.MY_VERSION |> Text + +# ╔═╡ 00000000-0000-0000-0000-000000000001 +PLUTO_PROJECT_TOML_CONTENTS = """ +[deps] +PlutoPkgTestA = "419c6f8d-b8cd-4309-abdc-cee491252f94" + +[compat] +PlutoPkgTestA = "~0.2.2" +""" + +# ╔═╡ 00000000-0000-0000-0000-000000000002 +PLUTO_MANIFEST_TOML_CONTENTS = """ +# This file is FONSI-generated - editing it directly is AKSLD>FJKLAWEM +[afsd[ + asdf +((((( + [ + +123 123lkj 123lkj asdfkj asldkfj +[[ArgTools]]241f-c20a-4ad4-852c-f6b1247861c6" + +[[InteractiveUtil33d8-53b3-aaab-bd5110c3b7a0" +""" + +# ╔═╡ Cell order: +# ╠═1bb0c024-c7a9-11eb-216f-839261af9684 +# ╠═12fc51ea-0989-4bb2-a3a1-f1cc3bf2992d +# ╟─00000000-0000-0000-0000-000000000001 +# ╟─00000000-0000-0000-0000-000000000002 diff --git a/test/packages/old_artifacts_import.jl b/test/packages/old_artifacts_import.jl new file mode 100644 index 0000000000..0d8a613d9b --- /dev/null +++ b/test/packages/old_artifacts_import.jl @@ -0,0 +1,119 @@ +### A Pluto.jl notebook ### +# v0.15.0 + +using Markdown +using InteractiveUtils + +# ╔═╡ c581d17a-c965-11eb-1607-bbeb44933d25 +# This file imports an outdated version of PlutoPkgTestA: 0.2.1 (which is stored in the embedded Manifest file) and Artifacts, which is now a standard library (as of Julia 1.6), but it used to be a registered package (https://github.com/JuliaPackaging/Artifacts.jl). This notebook was generated on Julia 1.5, so the Manifest will be very very confusing for Julia 1.6 and up. + +# It is generated on Julia 1.5 (our oldest supported Julia version, Manifest.toml is not backwards-compatible): + +# 1. add our test registry: +# pkg> registry add https://github.com/JuliaPluto/PlutoPkgTestRegistry + +# 2. using Pluto, open the simple_import.jl notebook + +# 3. add the `import Artifacts` cell + + + +import PlutoPkgTestA + +# ╔═╡ aef57966-ea36-478f-8724-e71430f10be9 +PlutoPkgTestA.MY_VERSION |> Text + +# ╔═╡ f9bdbb35-4326-4786-b308-88b6894923df +import Artifacts + +# ╔═╡ 00000000-0000-0000-0000-000000000001 +PLUTO_PROJECT_TOML_CONTENTS = """ +[deps] +Artifacts = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" +PlutoPkgTestA = "419c6f8d-b8cd-4309-abdc-cee491252f94" + +[compat] +Artifacts = "~1.3.0" +PlutoPkgTestA = "~0.2.2" +""" + +# ╔═╡ 00000000-0000-0000-0000-000000000002 +PLUTO_MANIFEST_TOML_CONTENTS = """ +# This file is machine-generated - editing it directly is not advised + +[[Artifacts]] +deps = ["Pkg"] +git-tree-sha1 = "c30985d8821e0cd73870b17b0ed0ce6dc44cb744" +uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" +version = "1.3.0" + +[[Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" + +[[Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" + +[[InteractiveUtils]] +deps = ["Markdown"] +uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" + +[[LibGit2]] +deps = ["Printf"] +uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" + +[[Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" + +[[Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" + +[[Markdown]] +deps = ["Base64"] +uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" + +[[Pkg]] +deps = ["Dates", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "UUIDs"] +uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" + +[[PlutoPkgTestA]] +deps = ["Pkg"] +git-tree-sha1 = "6c9aa67135641123c559d59ba88e8cb93841773a" +uuid = "419c6f8d-b8cd-4309-abdc-cee491252f94" +version = "0.2.2" + +[[Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[[REPL]] +deps = ["InteractiveUtils", "Markdown", "Sockets"] +uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" + +[[Random]] +deps = ["Serialization"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" + +[[SHA]] +uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" + +[[Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" + +[[Sockets]] +uuid = "6462fe0b-24de-5631-8697-dd941f90decc" + +[[UUIDs]] +deps = ["Random", "SHA"] +uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" + +[[Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" +""" + +# ╔═╡ Cell order: +# ╠═c581d17a-c965-11eb-1607-bbeb44933d25 +# ╠═aef57966-ea36-478f-8724-e71430f10be9 +# ╠═f9bdbb35-4326-4786-b308-88b6894923df +# ╟─00000000-0000-0000-0000-000000000001 +# ╟─00000000-0000-0000-0000-000000000002 diff --git a/test/packages/old_import.jl b/test/packages/old_import.jl new file mode 100644 index 0000000000..ea7733aa83 --- /dev/null +++ b/test/packages/old_import.jl @@ -0,0 +1,19 @@ +### A Pluto.jl notebook ### +# v0.14.7 + +using Markdown +using InteractiveUtils + +# ╔═╡ 22364cc8-c792-11eb-3458-75afd80f5a03 +using PlutoPkgTestA + +# ╔═╡ ca0765b8-ce3f-4869-bd65-855905d49a2d +using Dates + +# ╔═╡ 5cbe4ac1-1bc5-4ef1-95ce-e09749343088 +domath(20) + +# ╔═╡ Cell order: +# ╠═22364cc8-c792-11eb-3458-75afd80f5a03 +# ╠═ca0765b8-ce3f-4869-bd65-855905d49a2d +# ╠═5cbe4ac1-1bc5-4ef1-95ce-e09749343088 diff --git a/test/packages/simple_import.jl b/test/packages/simple_import.jl new file mode 100644 index 0000000000..c741ae26a3 --- /dev/null +++ b/test/packages/simple_import.jl @@ -0,0 +1,109 @@ +### A Pluto.jl notebook ### +# v0.15.0 + +using Markdown +using InteractiveUtils + +# ╔═╡ c581d17a-c965-11eb-1607-bbeb44933d25 +# This file imports an outdated version of PlutoPkgTestA: 0.2.2 (which is stored in the embedded Manifest file). + +# It is generated on Julia 1.5 (our oldest supported Julia version, Manifest.toml is not backwards-compatible): + +# 1. add our test registry: +# pkg> registry add https://github.com/JuliaPluto/PlutoPkgTestRegistry + +# 2. using Pluto, create this notebook + +# 3. use `Pluto.activate_notebook_environment` to change the version of PlutoPkgTestA to 0.2.2 + +# 4. open the notebook in Pluto again. Add a second package (PlutoPkgTestD) and then remove it again. This adds the auto-generated compat entry for PlutoPkgTestA. + + + +import PlutoPkgTestA + +# ╔═╡ aef57966-ea36-478f-8724-e71430f10be9 +PlutoPkgTestA.MY_VERSION |> Text + +# ╔═╡ 00000000-0000-0000-0000-000000000001 +PLUTO_PROJECT_TOML_CONTENTS = """ +[deps] +PlutoPkgTestA = "419c6f8d-b8cd-4309-abdc-cee491252f94" + +[compat] +PlutoPkgTestA = "~0.2.2" +""" + +# ╔═╡ 00000000-0000-0000-0000-000000000002 +PLUTO_MANIFEST_TOML_CONTENTS = """ +# This file is machine-generated - editing it directly is not advised + +[[Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" + +[[Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" + +[[InteractiveUtils]] +deps = ["Markdown"] +uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" + +[[LibGit2]] +deps = ["Printf"] +uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" + +[[Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" + +[[Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" + +[[Markdown]] +deps = ["Base64"] +uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" + +[[Pkg]] +deps = ["Dates", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "UUIDs"] +uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" + +[[PlutoPkgTestA]] +deps = ["Pkg"] +git-tree-sha1 = "6c9aa67135641123c559d59ba88e8cb93841773a" +uuid = "419c6f8d-b8cd-4309-abdc-cee491252f94" +version = "0.2.2" + +[[Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[[REPL]] +deps = ["InteractiveUtils", "Markdown", "Sockets"] +uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" + +[[Random]] +deps = ["Serialization"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" + +[[SHA]] +uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" + +[[Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" + +[[Sockets]] +uuid = "6462fe0b-24de-5631-8697-dd941f90decc" + +[[UUIDs]] +deps = ["Random", "SHA"] +uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" + +[[Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" +""" + +# ╔═╡ Cell order: +# ╠═c581d17a-c965-11eb-1607-bbeb44933d25 +# ╠═aef57966-ea36-478f-8724-e71430f10be9 +# ╟─00000000-0000-0000-0000-000000000001 +# ╟─00000000-0000-0000-0000-000000000002 diff --git a/test/packages/unregistered_import.jl b/test/packages/unregistered_import.jl new file mode 100644 index 0000000000..31c6ca3f05 --- /dev/null +++ b/test/packages/unregistered_import.jl @@ -0,0 +1,98 @@ +### A Pluto.jl notebook ### +# v0.15.0 + +using Markdown +using InteractiveUtils + +# ╔═╡ c581d17a-c965-11eb-1607-bbeb44933d25 +# This file imports a package that is not registered: PlutoPkgTestZZZZ, and yet it is included in the embedded Manifest. This can happen when creating a notebook in a future version of Julia, or using a custom registry. + +# This file was created by editing `simple_import.jl` by hand. + +import PlutoPkgTestZZZZ + +# ╔═╡ aef57966-ea36-478f-8724-e71430f10be9 +PlutoPkgTestZZZZ.MY_VERSION |> Text + +# ╔═╡ 00000000-0000-0000-0000-000000000001 +PLUTO_PROJECT_TOML_CONTENTS = """ +[deps] +PlutoPkgTestZZZZ = "99999999-b8cd-4309-abdc-cee491252f94"[deps] + +[compat] +PlutoPkgTestZZZZ = "~0.2.2" +""" + +# ╔═╡ 00000000-0000-0000-0000-000000000002 +PLUTO_MANIFEST_TOML_CONTENTS = """ +# This file is machine-generated - editing it directly is not advised + +[[Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" + +[[Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" + +[[InteractiveUtils]] +deps = ["Markdown"] +uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" + +[[LibGit2]] +deps = ["Printf"] +uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" + +[[Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" + +[[Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" + +[[Markdown]] +deps = ["Base64"] +uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" + +[[Pkg]] +deps = ["Dates", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "UUIDs"] +uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" + +[[PlutoPkgTestZZZZ]] +deps = ["Pkg"] +git-tree-sha1 = "6c9aa67135641123c559d59ba88e8cb93841773a" +uuid = "99999999-b8cd-4309-abdc-cee491252f94" +version = "0.2.2" + +[[Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[[REPL]] +deps = ["InteractiveUtils", "Markdown", "Sockets"] +uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" + +[[Random]] +deps = ["Serialization"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" + +[[SHA]] +uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" + +[[Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" + +[[Sockets]] +uuid = "6462fe0b-24de-5631-8697-dd941f90decc" + +[[UUIDs]] +deps = ["Random", "SHA"] +uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" + +[[Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" +""" + +# ╔═╡ Cell order: +# ╠═c581d17a-c965-11eb-1607-bbeb44933d25 +# ╠═aef57966-ea36-478f-8724-e71430f10be9 +# ╟─00000000-0000-0000-0000-000000000001 +# ╟─00000000-0000-0000-0000-000000000002 diff --git a/test/runtests.jl b/test/runtests.jl index abf7260972..0b7d248737 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,12 +1,20 @@ include("./helpers.jl") + +# tests that start new processes: include("./WorkspaceManager.jl") -include("./RichOutput.jl") +include("./packages/Basic.jl") +VERSION > v"1.6.99" || include("./RichOutput.jl") include("./React.jl") -include("./ExpressionExplorer.jl") include("./Dynamic.jl") -include("./MethodSignatures.jl") + +# for SOME reason 😞 the Notebook.jl tests need to run AFTER all the tests above, or the Github Actions runner on Windows gets internal julia errors. include("./Notebook.jl") -include("./Configuration.jl") + +# tests that don't start new processes: +include("./packages/PkgCompat.jl") +include("./ExpressionExplorer.jl") +include("./MethodSignatures.jl") +VERSION > v"1.6.99" || include("./Configuration.jl") include("./Analysis.jl") include("./Firebasey.jl") include("./DependencyCache.jl")