Skip to content

Commit 3452750

Browse files
authored
Move install (#8)
* Python core installer 1.5.0 * move install core installler python script * v12.2.0 * fix pio install add tests * add more tests * enhance tests * fixes * fix install uv in penv * remove obsolete call which errors * v12.3.0
1 parent bd23b98 commit 3452750

17 files changed

+2471
-133
lines changed

dist/index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "pioarduino-node-helpers",
3-
"version": "12.1.3",
3+
"version": "12.3.0",
44
"description": "Collection of Node.JS helpers for PlatformIO fork pioarduino",
55
"main": "dist/index.js",
66
"engines": {
@@ -10,7 +10,15 @@
1010
"build": "webpack --env build",
1111
"dev": "webpack --progress --watch --env dev",
1212
"lint": "eslint --ext js src || exit 0",
13-
"format": "prettier --single-quote --print-width 88 --write \"src/**/*.js\""
13+
"format": "prettier --single-quote --print-width 88 --write \"src/**/*.js\"",
14+
"test": "node test/test-uv-only.mjs && node test/test-pioarduino-script.mjs && node test/test-installer-script.mjs && node test/test-full-installation.mjs && node test/test-uv-platformio-install.mjs",
15+
"test:uv": "node test/test-uv-only.mjs",
16+
"test:pioarduino": "node test/test-pioarduino-script.mjs",
17+
"test:installer": "node test/test-installer-script.mjs",
18+
"test:full": "node test/test-full-installation.mjs",
19+
"test:install": "node test/test-uv-platformio-install.mjs",
20+
"test:install-old": "node test/test-python-installer-execution.mjs",
21+
"test:manual": "node test/manual-test.js"
1422
},
1523
"repository": {
1624
"type": "git",
@@ -57,4 +65,4 @@
5765
"webpack": "~5.97.1",
5866
"webpack-cli": "~6.0.1"
5967
}
60-
}
68+
}

src/installer/get-pioarduino.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/installer/get-python.js

Lines changed: 91 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
* the root directory of this source tree.
77
*/
88

9-
import * as proc from '../proc';
10-
import { callInstallerScript } from './get-pioarduino';
9+
import * as proc from '../proc.js';
10+
import { callInstallerScript } from './get-pioarduino.js';
1111
import fs from 'fs';
1212
import path from 'path';
1313
import { promisify } from 'util';
@@ -208,16 +208,14 @@ async function getUVCommand() {
208208
}
209209

210210
/**
211-
* Install Python using UV package manager
212-
* Creates a virtual environment using `uv venv` with Python 3.13
213-
* This is simpler and more reliable than installing Python separately
214-
* @param {string} destinationDir - Target installation directory (venv path)
215-
* @param {string} pythonVersion - Python version to install (default: "3.13")
216-
* @returns {Promise<string>} Path to installed Python venv directory
217-
* @throws {Error} If UV installation or venv creation fails
211+
* Ensure Python is available via UV
212+
* UV will automatically download and manage Python if needed
213+
* @param {string} pythonVersion - Python version to ensure (default: "3.13")
214+
* @returns {Promise<string>} Path to UV-managed Python executable
215+
* @throws {Error} If UV installation or Python download fails
218216
*/
219-
async function installPythonWithUV(destinationDir, pythonVersion = '3.13') {
220-
log('info', `Creating Python ${pythonVersion} venv using UV`);
217+
async function ensurePythonWithUV(pythonVersion = '3.13') {
218+
log('info', `Ensuring Python ${pythonVersion} is available via UV`);
221219

222220
// Ensure UV is available, install if necessary
223221
if (!(await isUVAvailable())) {
@@ -228,118 +226,121 @@ async function installPythonWithUV(destinationDir, pythonVersion = '3.13') {
228226
const uvCommand = await getUVCommand();
229227
log('info', `Using UV command: ${uvCommand}`);
230228

231-
// Clean up any existing installation to avoid conflicts
232229
try {
233-
await fs.promises.rm(destinationDir, { recursive: true, force: true });
234-
} catch (err) {
235-
// Ignore cleanup errors (directory might not exist)
236-
}
237-
238-
try {
239-
// Create venv directly using uv venv command with absolute path
240-
const absolutePath = path.resolve(destinationDir);
230+
// First check if Python is already installed
231+
try {
232+
const existingPath = await getUVPythonPath(pythonVersion);
233+
log('info', `Python ${pythonVersion} already available at: ${existingPath}`);
234+
return existingPath;
235+
} catch {
236+
// Python not found, need to install
237+
log('info', `Python ${pythonVersion} not found, installing...`);
238+
}
241239

242-
// Use --python-preference managed to allow UV to download Python if not found on system
243-
await execFile(
240+
// Use 'uv python install' to ensure Python is available
241+
// UV will download and manage Python automatically
242+
const installResult = await execFile(
244243
uvCommand,
245-
[
246-
'venv',
247-
absolutePath,
248-
'--python',
249-
pythonVersion,
250-
'--python-preference',
251-
'managed',
252-
],
244+
['python', 'install', pythonVersion],
253245
{
254-
timeout: 300000, // 5 minutes timeout for download and installation
246+
timeout: 300000, // 5 minutes timeout for download
255247
},
256248
);
257249

258-
// Verify that Python executable was successfully created
259-
await ensurePythonExeExists(destinationDir);
250+
log('info', `UV Python install output: ${installResult.stdout}`);
260251

261-
log('info', `Python ${pythonVersion} venv created successfully: ${destinationDir}`);
262-
return destinationDir;
252+
// Get the path to the UV-managed Python
253+
const pythonPath = await getUVPythonPath(pythonVersion);
254+
log('info', `UV-managed Python ${pythonVersion} installed at: ${pythonPath}`);
255+
return pythonPath;
263256
} catch (err) {
264-
throw new Error(`UV venv creation failed: ${err.message}`);
257+
throw new Error(`UV Python installation failed: ${err.message}`);
265258
}
266259
}
267260

268261
/**
269-
* Verify that Python executable exists in the venv directory
270-
* Checks the standard venv bin/Scripts directory for Python executable
271-
* @param {string} pythonDir - Directory containing Python venv
272-
* @returns {Promise<boolean>} True if executable exists and is accessible
273-
* @throws {Error} If no Python executable found in expected locations
262+
* Get the path to UV-managed Python executable
263+
* @param {string} pythonVersion - Python version (default: "3.13")
264+
* @returns {Promise<string>} Path to UV-managed Python executable
265+
* @throws {Error} If Python is not found
274266
*/
275-
async function ensurePythonExeExists(pythonDir) {
276-
// Standard venv structure: bin/ on Unix, Scripts/ on Windows
277-
const binDir = proc.IS_WINDOWS
278-
? path.join(pythonDir, 'Scripts')
279-
: path.join(pythonDir, 'bin');
280-
const executables = proc.IS_WINDOWS ? ['python.exe'] : ['python3', 'python'];
281-
282-
for (const exeName of executables) {
267+
async function getUVPythonPath(pythonVersion = '3.13') {
268+
const uvCommand = await getUVCommand();
269+
270+
try {
271+
const result = await execFile(uvCommand, ['python', 'find', pythonVersion], {
272+
timeout: 10000,
273+
});
274+
275+
let pythonPath = result.stdout.trim();
276+
if (!pythonPath) {
277+
throw new Error('UV did not return a Python path');
278+
}
279+
280+
// Normalize path for the current platform
281+
pythonPath = path.normalize(pythonPath);
282+
283+
// Verify the executable exists
283284
try {
284-
await fs.promises.access(path.join(binDir, exeName));
285-
return true;
286-
} catch (err) {
287-
// Continue trying other executables
285+
await fs.promises.access(pythonPath, fs.constants.X_OK);
286+
} catch (accessErr) {
287+
// On Windows, try adding .exe if not present
288+
if (proc.IS_WINDOWS && !pythonPath.endsWith('.exe')) {
289+
const pythonPathWithExe = pythonPath + '.exe';
290+
try {
291+
await fs.promises.access(pythonPathWithExe, fs.constants.X_OK);
292+
pythonPath = pythonPathWithExe;
293+
} catch {
294+
throw new Error(`Python executable not accessible at: ${pythonPath}`);
295+
}
296+
} else {
297+
throw new Error(`Python executable not accessible at: ${pythonPath}`);
298+
}
288299
}
289-
}
290300

291-
throw new Error('Python executable does not exist after venv creation!');
301+
log('info', `Verified UV-managed Python at: ${pythonPath}`);
302+
return pythonPath;
303+
} catch (err) {
304+
throw new Error(`Could not find UV-managed Python: ${err.message}`);
305+
}
292306
}
293307

294308
/**
295-
* Main entry point for installing Python distribution using UV
296-
* This replaces the legacy complex installation logic with a simple UV-based approach
297-
* @param {string} destinationDir - Target installation directory
298-
* @param {object} options - Optional configuration (kept for API compatibility)
299-
* @returns {Promise<string>} Path to installed Python directory
309+
* Main entry point for ensuring Python is available via UV
310+
* UV will download and manage Python automatically, no venv needed
311+
* @returns {Promise<string>} Path to UV-managed Python executable
300312
* @throws {Error} If Python installation fails for any reason
301313
*/
302-
export async function installPortablePython(destinationDir) {
303-
log('info', 'Starting Python 3.13 installation');
314+
export async function installPortablePython() {
315+
log('info', 'Ensuring Python 3.13 is available via UV');
304316

305-
// UV-based installation is now the only supported method
306317
try {
307-
return await installPythonWithUV(destinationDir, '3.13');
318+
// Ensure Python is available via UV (will download if needed)
319+
const pythonPath = await ensurePythonWithUV('3.13');
320+
log('info', `Python available at: ${pythonPath}`);
321+
return pythonPath;
308322
} catch (uvError) {
309-
log('error', `UV installation failed: ${uvError.message}`);
323+
log('error', `UV Python setup failed: ${uvError.message}`);
310324
throw new Error(
311325
`Python installation failed: ${uvError.message}. Please ensure UV can be installed and internet connection is available.`,
312326
);
313327
}
314328
}
315329

316330
/**
317-
* Locate Python executable in a venv directory
318-
* Uses standard venv structure (bin/ on Unix, Scripts/ on Windows)
319-
* @param {string} pythonDir - Python venv directory to search
320-
* @returns {Promise<string>} Full path to Python executable
321-
* @throws {Error} If no executable found in the venv
331+
* Get the path to UV-managed Python executable
332+
* @param {string} pythonVersion - Python version (default: "3.13")
333+
* @returns {Promise<string>} Path to UV-managed Python executable
322334
*/
323-
function getPythonExecutablePath(pythonDir) {
324-
// Standard venv structure
325-
const binDir = proc.IS_WINDOWS
326-
? path.join(pythonDir, 'Scripts')
327-
: path.join(pythonDir, 'bin');
328-
const executables = proc.IS_WINDOWS ? ['python.exe'] : ['python3', 'python'];
329-
330-
for (const exeName of executables) {
331-
const fullPath = path.join(binDir, exeName);
332-
try {
333-
fs.accessSync(fullPath, fs.constants.X_OK);
334-
log('info', `Found Python executable: ${fullPath}`);
335-
return fullPath;
336-
} catch (err) {
337-
// Continue searching through all executables
338-
}
339-
}
340-
341-
throw new Error(`Could not find Python executable in venv ${pythonDir}`);
335+
async function getPythonExecutablePath(pythonVersion = '3.13') {
336+
return await getUVPythonPath(pythonVersion);
342337
}
343338

344339
// Export utility functions for external use
345-
export { isPythonVersionCompatible, isUVAvailable, installUV, getPythonExecutablePath };
340+
export {
341+
isPythonVersionCompatible,
342+
isUVAvailable,
343+
installUV,
344+
getPythonExecutablePath,
345+
getUVCommand,
346+
};

src/installer/stages/pioarduino-core.js

Lines changed: 37 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
import * as core from '../../core';
1010
import * as misc from '../../misc';
1111
import * as proc from '../../proc';
12-
import { findPythonExecutable, installPortablePython } from '../get-python';
12+
import {
13+
findPythonExecutable,
14+
getPythonExecutablePath,
15+
installPortablePython,
16+
} from '../get-python';
1317

1418
import BaseStage from './base';
1519
import { callInstallerScript } from '../get-pioarduino';
@@ -344,33 +348,30 @@ export default class pioarduinoCoreStage extends BaseStage {
344348

345349
async whereIsPython({ prompt = false } = {}) {
346350
let status = this.params.pythonPrompt.STATUS_TRY_AGAIN;
347-
// Don't call configureBuiltInPython() here - PATH already set in check()
348351

349352
if (!prompt) {
350-
// First try to find Python in the built-in location if available
353+
// First try to find UV-managed Python if built-in Python is enabled
351354
if (this.params.useBuiltinPython) {
352355
try {
353-
const pythonPath = await pioarduinoCoreStage.findBuiltInPythonExe();
354-
await fs.access(pythonPath);
355-
console.info('Using built-in Python:', pythonPath);
356+
const pythonPath = await getPythonExecutablePath('3.13');
357+
console.info('Using UV-managed Python:', pythonPath);
356358
return pythonPath;
357359
} catch (err) {
358-
console.info('Built-in Python not found, searching system PATH');
360+
console.info('UV-managed Python not found, searching system PATH');
359361
}
360362
}
361363
return await findPythonExecutable();
362364
}
363365

364366
do {
365-
// First try to find built-in Python if enabled
367+
// First try to find UV-managed Python if enabled
366368
if (this.params.useBuiltinPython) {
367369
try {
368-
const pythonPath = await pioarduinoCoreStage.findBuiltInPythonExe();
369-
await fs.access(pythonPath);
370-
console.info('Using built-in Python:', pythonPath);
370+
const pythonPath = await getPythonExecutablePath('3.13');
371+
console.info('Using UV-managed Python:', pythonPath);
371372
return pythonPath;
372373
} catch (err) {
373-
console.info('Built-in Python not found, searching system PATH');
374+
console.info('UV-managed Python not found, searching system PATH');
374375
}
375376
}
376377

@@ -413,38 +414,38 @@ export default class pioarduinoCoreStage extends BaseStage {
413414
}
414415
withProgress('Preparing for installation', 10);
415416
try {
417+
let uvPythonPath = null;
416418
if (this.params.useBuiltinPython) {
417-
withProgress('Downloading portable Python interpreter', 10);
419+
withProgress('Installing Python 3.13 using UV', 10);
418420
try {
419-
await installPortablePython(pioarduinoCoreStage.getBuiltInPythonDir(), {
420-
predownloadedPackageDir: this.params.predownloadedPackageDir,
421-
});
421+
// installPortablePython now returns the Python executable path directly
422+
uvPythonPath = await installPortablePython();
423+
console.info('UV-managed Python installed at:', uvPythonPath);
422424
} catch (err) {
423-
console.warn(err);
424-
// cleanup
425-
try {
426-
await fs.rm(pioarduinoCoreStage.getBuiltInPythonDir(), {
427-
recursive: true,
428-
force: true,
429-
});
430-
} catch (err) {}
425+
console.warn('UV Python installation failed:', err);
426+
throw err;
431427
}
432428
}
433429

434430
withProgress('Installing pioarduino Core', 20);
435-
const scriptArgs = [];
436-
if (this.useDevCore()) {
437-
scriptArgs.push('--dev');
438-
}
439-
console.info(
440-
await callInstallerScript(
441-
await this.whereIsPython({ prompt: true }),
442-
scriptArgs,
443-
),
444-
);
445431

446-
// Check that PIO Core is installed, load its state and patch OS environment
447-
withProgress('Loading pioarduino Core state', 40);
432+
// Use the Python installer script to set up penv with UV
433+
const pythonToUse = uvPythonPath || (await this.whereIsPython({ prompt: true }));
434+
console.info('Using Python for PlatformIO installation:', pythonToUse);
435+
436+
// Use the installer script to create penv and install PlatformIO
437+
withProgress('Creating virtual environment and installing PlatformIO', 30);
438+
439+
// Note: The 'install' command doesn't support --dev, --version-spec, or --no-auto-upgrade
440+
// These options are only available for the 'check' command
441+
const scriptArgs = ['install'];
442+
443+
console.info('Running installer script with args:', scriptArgs);
444+
const installOutput = await callInstallerScript(pythonToUse, scriptArgs);
445+
console.info('PlatformIO installation output:', installOutput);
446+
447+
// Load the core state from the installer script
448+
withProgress('Loading pioarduino Core state', 80);
448449
await this.loadCoreState();
449450

450451
withProgress('Installing pioarduino Home', 80);

0 commit comments

Comments
 (0)