diff --git a/src/app.js b/src/app.js index 244a368d..f91634bc 100644 --- a/src/app.js +++ b/src/app.js @@ -31,7 +31,7 @@ import { getPkgIndexes, rawInstallPkg, fetchPkgReadme } from './package_mgr.js' import { ConnectionUID } from './connection_uid.js' import translations from '../build/translations.json' import { parseStackTrace, validatePython, disassembleMPY, minifyPython, prettifyPython, compilePython, reprStr } from './python_utils.js' -import { SYSTEM_DIRS } from './emulator.js' +import { SYSTEM_DIRS, visibleTree } from './emulator.js' import { createPyDevicesVM, enableSimulatorAudio, armSimulatorAudio, disarmSimulatorAudio } from './pydevices/runtime.js' import { showDeviceStage, hideDeviceStage, initDeviceStage, onDisplayResize, getDisplayConfig } from './pydevices/stage.js' import { resolveGalleryExample, rawStageGalleryExample } from './pydevices/gallery.js' @@ -1236,7 +1236,9 @@ async function _raw_updateFileTree(raw) { console.log(err) } - const fs_tree = await raw.walkFs() + /* The simulator's runtime keeps folders of its own; a real board's are all the user's */ + const walked = await raw.walkFs() + const fs_tree = (connType === 'sim') ? visibleTree(walked) : walked const delta = fsCache.reconcileListing(fs_tree, isSpecialPath) _updateFileTree(fs_tree, fs_stats); diff --git a/src/emulator.js b/src/emulator.js index 05451746..adc99cbc 100644 --- a/src/emulator.js +++ b/src/emulator.js @@ -6,12 +6,12 @@ * This includes no assurances about being fit for any specific purpose. */ -import { MicroPythonWASM, SYSTEM_DIRS } from './transports/vm.js' +import { MicroPythonWASM, SYSTEM_DIRS, visibleTree } from './transports/vm.js' import { loadVFS } from './python_utils.js' import { loadMicroPython } from '@micropython/micropython-webassembly-pyscript/micropython.mjs' import i18next from 'i18next' -export { MicroPythonWASM, SYSTEM_DIRS } +export { MicroPythonWASM, SYSTEM_DIRS, visibleTree } const T = i18next.t.bind(i18next) diff --git a/src/pydevices/runtime.js b/src/pydevices/runtime.js index be2295c4..f23a4d63 100644 --- a/src/pydevices/runtime.js +++ b/src/pydevices/runtime.js @@ -74,7 +74,7 @@ import os, sys # Absolute /lib, matching a board: the package manager finds the install # target by looking for a sys.path entry that ends with "/lib", and a # relative "lib" left it refusing to install anything on the simulator. -sys.path[:] = [".", ".frozen", "/lib", "/utils"] +sys.path[:] = [".", ".frozen", "/lib", "/utils", "/.shim"] from boarddev import env_set env_set("PYDEVICES_WIDTH", ${Number(width)}) env_set("PYDEVICES_HEIGHT", ${Number(height)}) @@ -181,6 +181,8 @@ export function createPyDevicesVM(opts = {}) { const port = new MicroPythonWASM(loadPyDevicesMicroPython, { wasmURL: runtimeURL('micropython.wasm'), infoType: 'PyDevices Simulator', + // machine.py's home: on sys.path (see bootScript) but not in the user's lib/ + shimDir: '/.shim', // The build is compiled with ASYNCIFY, so the VM can yield to the // browser instead of locking the tab up for the length of a sleep. asyncify: true, diff --git a/src/transports/vm.js b/src/transports/vm.js index 886f59cc..e65aba8f 100644 --- a/src/transports/vm.js +++ b/src/transports/vm.js @@ -12,6 +12,16 @@ import { Transport } from './base.js' const PYEXEC_FORCED_EXIT = 0x100 export const SYSTEM_DIRS = new Set(['/sys', '/dev', '/proc']) +/* Top-level entries the runtime itself needs (the interpreter's /tmp and + /dev, the emulated home, the shim folder below) but that are not the user's + files. They stay on the device; the File Manager just does not list them. */ +export const HIDDEN_ROOTS = new Set(['/dev', '/home', '/proc', '/sys', '/tmp', '/.shim']) + +/** Drops the hidden top-level entries from a walkFs() tree. */ +export function visibleTree(tree) { + return tree.filter((n) => !HIDDEN_ROOTS.has(n.path)) +} + const MACHINE_MODULE = `\ def reset(): raise SystemExit @@ -99,6 +109,7 @@ export class MicroPythonWASM extends Transport { this._inRawMode = false this._suppressedOutput = false this._fsPopulated = false + this._shimDir = opts.shimDir || '/lib' this._decoder = new TextDecoder('utf-8') this._writeQueue = Promise.resolve() this.info = { type: opts.infoType || 'MicroPython WASM' } @@ -135,9 +146,13 @@ export class MicroPythonWASM extends Transport { try { this.mp.FS.mkdir('/lib') } catch { /* exists */ } } - // Always ensure machine module is present + // Always ensure machine module is present. It lives in shimDir, which a + // runtime can point away from /lib so the user's lib/ starts empty. try { this.mp.FS.mkdir('/lib') } catch { /* exists */ } - this.mp.FS.writeFile('/lib/machine.py', MACHINE_MODULE) + if (this._shimDir !== '/lib') { + try { this.mp.FS.mkdir(this._shimDir) } catch { /* exists */ } + } + this.mp.FS.writeFile(this._shimDir + '/machine.py', MACHINE_MODULE) // Interpreter state - sys.path, environment, imports - does not survive // into the fresh VM a reset builds, so this runs for every one of them, diff --git a/test/browser/boot-file-tree.spec.js b/test/browser/boot-file-tree.spec.js new file mode 100644 index 00000000..8cfcfc0c --- /dev/null +++ b/test/browser/boot-file-tree.spec.js @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +// +// The simulator's File Manager used to list the runtime's own folders (dev, +// home, proc, tmp) and a machine.py in lib/ next to the examples. A fresh +// simulator shows the examples, main.py and an empty lib/ for installed +// packages, nothing else. +import { test, expect } from '@playwright/test' +import { connectSimulator } from './helpers.js' + +test('a fresh simulator lists only the examples, main.py and an empty lib/', async ({ page }) => { + await connectSimulator(page) + const rows = page.locator('#menu-file-tree .tree-row[data-path]') + await expect(rows.filter({ hasText: 'main.py' })).toHaveCount(1) + const paths = await rows.evaluateAll((els) => els.map((e) => e.getAttribute('data-path'))) + expect(paths.sort()).toEqual([ + '/', + '/00_touch_paint.py', + '/01_lvgl_counter.py', + '/02_lvgl_thermostat.py', + '/03_pdwidgets_dashboard.py', + '/04_pygraphics_shapes.py', + '/05_displaydev_raw.py', + '/lib', + '/main.py', + ]) +}) + +test('the interpreter still has what it needs after the tree is trimmed', async ({ page }) => { + await connectSimulator(page) + await page.locator('#xterm').click() + await page.keyboard.type("import os, machine; print('TMP', os.stat('/tmp') is not None, hasattr(machine, 'reset'))\n") + await expect(page.locator('#xterm')).toContainText('TMP True True') +})