Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions src/emulator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 3 additions & 1 deletion src/pydevices/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)})
Expand Down Expand Up @@ -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,
Expand Down
19 changes: 17 additions & 2 deletions src/transports/vm.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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' }
Expand Down Expand Up @@ -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,
Expand Down
33 changes: 33 additions & 0 deletions test/browser/boot-file-tree.spec.js
Original file line number Diff line number Diff line change
@@ -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')
})
Loading