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
2 changes: 1 addition & 1 deletion dist/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

14 changes: 11 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pioarduino-node-helpers",
"version": "12.1.3",
"version": "12.3.0",
"description": "Collection of Node.JS helpers for PlatformIO fork pioarduino",
"main": "dist/index.js",
"engines": {
Expand All @@ -10,7 +10,15 @@
"build": "webpack --env build",
"dev": "webpack --progress --watch --env dev",
"lint": "eslint --ext js src || exit 0",
"format": "prettier --single-quote --print-width 88 --write \"src/**/*.js\""
"format": "prettier --single-quote --print-width 88 --write \"src/**/*.js\"",
"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",
"test:uv": "node test/test-uv-only.mjs",
"test:pioarduino": "node test/test-pioarduino-script.mjs",
"test:installer": "node test/test-installer-script.mjs",
"test:full": "node test/test-full-installation.mjs",
"test:install": "node test/test-uv-platformio-install.mjs",
"test:install-old": "node test/test-python-installer-execution.mjs",
"test:manual": "node test/manual-test.js"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -57,4 +65,4 @@
"webpack": "~5.97.1",
"webpack-cli": "~6.0.1"
}
}
}
4 changes: 2 additions & 2 deletions src/installer/get-pioarduino.js

Large diffs are not rendered by default.

181 changes: 91 additions & 90 deletions src/installer/get-python.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
* the root directory of this source tree.
*/

import * as proc from '../proc';
import { callInstallerScript } from './get-pioarduino';
import * as proc from '../proc.js';
import { callInstallerScript } from './get-pioarduino.js';
import fs from 'fs';
import path from 'path';
import { promisify } from 'util';
Expand Down Expand Up @@ -208,16 +208,14 @@ async function getUVCommand() {
}

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

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

// Clean up any existing installation to avoid conflicts
try {
await fs.promises.rm(destinationDir, { recursive: true, force: true });
} catch (err) {
// Ignore cleanup errors (directory might not exist)
}

try {
// Create venv directly using uv venv command with absolute path
const absolutePath = path.resolve(destinationDir);
// First check if Python is already installed
try {
const existingPath = await getUVPythonPath(pythonVersion);
log('info', `Python ${pythonVersion} already available at: ${existingPath}`);
return existingPath;
} catch {
// Python not found, need to install
log('info', `Python ${pythonVersion} not found, installing...`);
}

// Use --python-preference managed to allow UV to download Python if not found on system
await execFile(
// Use 'uv python install' to ensure Python is available
// UV will download and manage Python automatically
const installResult = await execFile(
uvCommand,
[
'venv',
absolutePath,
'--python',
pythonVersion,
'--python-preference',
'managed',
],
['python', 'install', pythonVersion],
{
timeout: 300000, // 5 minutes timeout for download and installation
timeout: 300000, // 5 minutes timeout for download
},
);

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

log('info', `Python ${pythonVersion} venv created successfully: ${destinationDir}`);
return destinationDir;
// Get the path to the UV-managed Python
const pythonPath = await getUVPythonPath(pythonVersion);
log('info', `UV-managed Python ${pythonVersion} installed at: ${pythonPath}`);
return pythonPath;
} catch (err) {
throw new Error(`UV venv creation failed: ${err.message}`);
throw new Error(`UV Python installation failed: ${err.message}`);
}
}

/**
* Verify that Python executable exists in the venv directory
* Checks the standard venv bin/Scripts directory for Python executable
* @param {string} pythonDir - Directory containing Python venv
* @returns {Promise<boolean>} True if executable exists and is accessible
* @throws {Error} If no Python executable found in expected locations
* Get the path to UV-managed Python executable
* @param {string} pythonVersion - Python version (default: "3.13")
* @returns {Promise<string>} Path to UV-managed Python executable
* @throws {Error} If Python is not found
*/
async function ensurePythonExeExists(pythonDir) {
// Standard venv structure: bin/ on Unix, Scripts/ on Windows
const binDir = proc.IS_WINDOWS
? path.join(pythonDir, 'Scripts')
: path.join(pythonDir, 'bin');
const executables = proc.IS_WINDOWS ? ['python.exe'] : ['python3', 'python'];

for (const exeName of executables) {
async function getUVPythonPath(pythonVersion = '3.13') {
const uvCommand = await getUVCommand();

try {
const result = await execFile(uvCommand, ['python', 'find', pythonVersion], {
timeout: 10000,
});

let pythonPath = result.stdout.trim();
if (!pythonPath) {
throw new Error('UV did not return a Python path');
}

// Normalize path for the current platform
pythonPath = path.normalize(pythonPath);

// Verify the executable exists
try {
await fs.promises.access(path.join(binDir, exeName));
return true;
} catch (err) {
// Continue trying other executables
await fs.promises.access(pythonPath, fs.constants.X_OK);
} catch (accessErr) {
// On Windows, try adding .exe if not present
if (proc.IS_WINDOWS && !pythonPath.endsWith('.exe')) {
const pythonPathWithExe = pythonPath + '.exe';
try {
await fs.promises.access(pythonPathWithExe, fs.constants.X_OK);
pythonPath = pythonPathWithExe;
} catch {
throw new Error(`Python executable not accessible at: ${pythonPath}`);
}
} else {
throw new Error(`Python executable not accessible at: ${pythonPath}`);
}
}
}

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

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

// UV-based installation is now the only supported method
try {
return await installPythonWithUV(destinationDir, '3.13');
// Ensure Python is available via UV (will download if needed)
const pythonPath = await ensurePythonWithUV('3.13');
log('info', `Python available at: ${pythonPath}`);
return pythonPath;
} catch (uvError) {
log('error', `UV installation failed: ${uvError.message}`);
log('error', `UV Python setup failed: ${uvError.message}`);
throw new Error(
`Python installation failed: ${uvError.message}. Please ensure UV can be installed and internet connection is available.`,
);
}
}

/**
* Locate Python executable in a venv directory
* Uses standard venv structure (bin/ on Unix, Scripts/ on Windows)
* @param {string} pythonDir - Python venv directory to search
* @returns {Promise<string>} Full path to Python executable
* @throws {Error} If no executable found in the venv
* Get the path to UV-managed Python executable
* @param {string} pythonVersion - Python version (default: "3.13")
* @returns {Promise<string>} Path to UV-managed Python executable
*/
function getPythonExecutablePath(pythonDir) {
// Standard venv structure
const binDir = proc.IS_WINDOWS
? path.join(pythonDir, 'Scripts')
: path.join(pythonDir, 'bin');
const executables = proc.IS_WINDOWS ? ['python.exe'] : ['python3', 'python'];

for (const exeName of executables) {
const fullPath = path.join(binDir, exeName);
try {
fs.accessSync(fullPath, fs.constants.X_OK);
log('info', `Found Python executable: ${fullPath}`);
return fullPath;
} catch (err) {
// Continue searching through all executables
}
}

throw new Error(`Could not find Python executable in venv ${pythonDir}`);
async function getPythonExecutablePath(pythonVersion = '3.13') {
return await getUVPythonPath(pythonVersion);
}

// Export utility functions for external use
export { isPythonVersionCompatible, isUVAvailable, installUV, getPythonExecutablePath };
export {
isPythonVersionCompatible,
isUVAvailable,
installUV,
getPythonExecutablePath,
getUVCommand,
};
73 changes: 37 additions & 36 deletions src/installer/stages/pioarduino-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
import * as core from '../../core';
import * as misc from '../../misc';
import * as proc from '../../proc';
import { findPythonExecutable, installPortablePython } from '../get-python';
import {
findPythonExecutable,
getPythonExecutablePath,
installPortablePython,
} from '../get-python';

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

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

if (!prompt) {
// First try to find Python in the built-in location if available
// First try to find UV-managed Python if built-in Python is enabled
if (this.params.useBuiltinPython) {
try {
const pythonPath = await pioarduinoCoreStage.findBuiltInPythonExe();
await fs.access(pythonPath);
console.info('Using built-in Python:', pythonPath);
const pythonPath = await getPythonExecutablePath('3.13');
console.info('Using UV-managed Python:', pythonPath);
return pythonPath;
} catch (err) {
console.info('Built-in Python not found, searching system PATH');
console.info('UV-managed Python not found, searching system PATH');
}
}
return await findPythonExecutable();
}

do {
// First try to find built-in Python if enabled
// First try to find UV-managed Python if enabled
if (this.params.useBuiltinPython) {
try {
const pythonPath = await pioarduinoCoreStage.findBuiltInPythonExe();
await fs.access(pythonPath);
console.info('Using built-in Python:', pythonPath);
const pythonPath = await getPythonExecutablePath('3.13');
console.info('Using UV-managed Python:', pythonPath);
return pythonPath;
} catch (err) {
console.info('Built-in Python not found, searching system PATH');
console.info('UV-managed Python not found, searching system PATH');
}
}

Expand Down Expand Up @@ -413,38 +414,38 @@ export default class pioarduinoCoreStage extends BaseStage {
}
withProgress('Preparing for installation', 10);
try {
let uvPythonPath = null;
if (this.params.useBuiltinPython) {
withProgress('Downloading portable Python interpreter', 10);
withProgress('Installing Python 3.13 using UV', 10);
try {
await installPortablePython(pioarduinoCoreStage.getBuiltInPythonDir(), {
predownloadedPackageDir: this.params.predownloadedPackageDir,
});
// installPortablePython now returns the Python executable path directly
uvPythonPath = await installPortablePython();
console.info('UV-managed Python installed at:', uvPythonPath);
} catch (err) {
console.warn(err);
// cleanup
try {
await fs.rm(pioarduinoCoreStage.getBuiltInPythonDir(), {
recursive: true,
force: true,
});
} catch (err) {}
console.warn('UV Python installation failed:', err);
throw err;
}
}

withProgress('Installing pioarduino Core', 20);
const scriptArgs = [];
if (this.useDevCore()) {
scriptArgs.push('--dev');
}
console.info(
await callInstallerScript(
await this.whereIsPython({ prompt: true }),
scriptArgs,
),
);

// Check that PIO Core is installed, load its state and patch OS environment
withProgress('Loading pioarduino Core state', 40);
// Use the Python installer script to set up penv with UV
const pythonToUse = uvPythonPath || (await this.whereIsPython({ prompt: true }));
console.info('Using Python for PlatformIO installation:', pythonToUse);

// Use the installer script to create penv and install PlatformIO
withProgress('Creating virtual environment and installing PlatformIO', 30);

// Note: The 'install' command doesn't support --dev, --version-spec, or --no-auto-upgrade
// These options are only available for the 'check' command
const scriptArgs = ['install'];

console.info('Running installer script with args:', scriptArgs);
const installOutput = await callInstallerScript(pythonToUse, scriptArgs);
console.info('PlatformIO installation output:', installOutput);

// Load the core state from the installer script
withProgress('Loading pioarduino Core state', 80);
await this.loadCoreState();

withProgress('Installing pioarduino Home', 80);
Expand Down
Loading