diff --git a/src/hub/reducers.test.ts b/src/hub/reducers.test.ts index 87b081a24..b65c2d3d3 100644 --- a/src/hub/reducers.test.ts +++ b/src/hub/reducers.test.ts @@ -58,6 +58,7 @@ test('initial state', () => { "runtime": "hub.runtime.disconnected", "selectedSlot": 0, "useLegacyDownload": false, + "useLegacyMainModule": false, "useLegacyStartUserProgram": false, "useLegacyStdio": false, } @@ -544,3 +545,23 @@ describe('useLegacyStartUserProgram', () => { ).toBeFalsy(); }); }); + +describe('useLegacyMainModule', () => { + test('Pybricks Profile < v1.5.0', () => { + expect( + reducers( + { useLegacyMainModule: false } as State, + bleDIServiceDidReceiveSoftwareRevision('1.4.0'), + ).useLegacyMainModule, + ).toBeTruthy(); + }); + + test('Pybricks Profile >= v1.5.0', () => { + expect( + reducers( + { useLegacyMainModule: true } as State, + bleDIServiceDidReceiveSoftwareRevision('1.5.0'), + ).useLegacyMainModule, + ).toBeFalsy(); + }); +}); diff --git a/src/hub/reducers.ts b/src/hub/reducers.ts index c4809250b..96df7f9ba 100644 --- a/src/hub/reducers.ts +++ b/src/hub/reducers.ts @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2020-2025 The Pybricks Authors +// Copyright (c) 2020-2026 The Pybricks Authors import { Reducer, combineReducers } from 'redux'; import * as semver from 'semver'; @@ -31,6 +31,7 @@ import { usbDidDisconnectPybricks, usbDidReceiveDeviceName, usbDidReceiveFirmwareRevision, + usbDidReceiveSoftwareRevision, usbDisconnectPybricks, } from '../usb/actions'; import { pythonVersionToSemver } from '../utils/version'; @@ -361,7 +362,10 @@ const useLegacyDownload: Reducer = (state = false, action) => { * When true, use NUS for stdio instead of Pybricks control characteristic. */ const useLegacyStdio: Reducer = (state = false, action) => { - if (bleDIServiceDidReceiveSoftwareRevision.matches(action)) { + if ( + bleDIServiceDidReceiveSoftwareRevision.matches(action) || + usbDidReceiveSoftwareRevision.matches(action) + ) { // Behavior changed starting with Pybricks Profile v1.3.0. return !semver.satisfies(action.version, '^1.3.0'); } @@ -373,7 +377,10 @@ const useLegacyStdio: Reducer = (state = false, action) => { * When true, use Legacy StartUserProgram. */ const useLegacyStartUserProgram: Reducer = (state = false, action) => { - if (bleDIServiceDidReceiveSoftwareRevision.matches(action)) { + if ( + bleDIServiceDidReceiveSoftwareRevision.matches(action) || + usbDidReceiveSoftwareRevision.matches(action) + ) { // Behavior changed starting with Pybricks Profile v1.4.0. return !semver.satisfies(action.version, '^1.4.0'); } @@ -381,6 +388,21 @@ const useLegacyStartUserProgram: Reducer = (state = false, action) => { return state; }; +/** + * When true, use the legacy `__main__` module name instead of the actual file name. + */ +const useLegacyMainModule: Reducer = (state = false, action) => { + if ( + bleDIServiceDidReceiveSoftwareRevision.matches(action) || + usbDidReceiveSoftwareRevision.matches(action) + ) { + // Behavior changed starting with Pybricks Profile v1.5.0. + return !semver.satisfies(action.version, '^1.5.0'); + } + + return state; +}; + /* * Returns number of available slots or 0 for slots not supported. */ @@ -418,6 +440,7 @@ export default combineReducers({ useLegacyDownload, useLegacyStdio, useLegacyStartUserProgram, + useLegacyMainModule, numOfSlots, selectedSlot, }); diff --git a/src/mpy/sagas.ts b/src/mpy/sagas.ts index b7b295980..b8c284a1e 100644 --- a/src/mpy/sagas.ts +++ b/src/mpy/sagas.ts @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2020-2023 The Pybricks Authors +// Copyright (c) 2020-2026 The Pybricks Authors import { compile as mpyCrossCompileV5 } from '@pybricks/mpy-cross-v5'; import { compile as mpyCrossCompileV6 } from '@pybricks/mpy-cross-v6'; @@ -104,8 +104,8 @@ function* handleCompile(action: ReturnType): Generator { /** * Compiles code into the Pybricks multi-mpy6 file format. * - * This includes a __main__ module which is the file currently open in the - * editor and any imported modules that can be found in the user file system. + * This includes the file currently open in the editor and any imported modules + * that can be found in the user file system. */ function* handleCompileMulti6(): Generator { // REVISIT: should we be getting the active file here or have it as an @@ -128,14 +128,22 @@ function* handleCompileMulti6(): Generator { return; } - const mainPy = yield* editorGetValue(); + const useLegacyMainModule = yield* select( + (s: RootState) => s.hub.useLegacyMainModule, + ); + + const mainPyContents = yield* editorGetValue(); + const mainPyPath = metadata.path ?? '__main__.py'; + const mainPyName = useLegacyMainModule + ? '__main__' + : mainPyPath.replace(/\.[^.]+$/, ''); const pyFiles = new Map([ - ['__main__', { path: metadata.path ?? '__main__.py', contents: mainPy }], + [mainPyName, { path: mainPyPath, contents: mainPyContents }], ]); - const checkedModules = new Set(['__main__']); - const uncheckedScripts = new Array(mainPy); + const checkedModules = new Set([mainPyName]); + const uncheckedScripts = new Array(mainPyContents); for (;;) { // parse all unchecked scripts to find imported modules that haven't