Skip to content

Commit 7bd464e

Browse files
committed
simplify / harden venv install
1 parent aa06861 commit 7bd464e

File tree

4 files changed

+50
-87
lines changed

4 files changed

+50
-87
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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "pioarduino-node-helpers",
3-
"version": "12.0.0",
3+
"version": "12.1.0",
44
"description": "Collection of Node.JS helpers for PlatformIO fork pioarduino",
55
"main": "dist/index.js",
66
"engines": {

src/installer/get-python.js

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

9-
import * as core from '../core';
109
import * as proc from '../proc';
1110
import { callInstallerScript } from './get-pioarduino';
1211
import fs from 'fs';
@@ -173,15 +172,15 @@ async function installUV() {
173172

174173
/**
175174
* Install Python using UV package manager
176-
* Uses UV to download and install Python from astral-sh/python-build-standalone
177-
* Automatically handles platform detection, download, verification, and extraction
178-
* @param {string} destinationDir - Target installation directory
175+
* Creates a virtual environment using `uv venv` with Python 3.13
176+
* This is simpler and more reliable than installing Python separately
177+
* @param {string} destinationDir - Target installation directory (venv path)
179178
* @param {string} pythonVersion - Python version to install (default: "3.13")
180-
* @returns {Promise<string>} Path to installed Python directory
181-
* @throws {Error} If UV installation or Python installation fails
179+
* @returns {Promise<string>} Path to installed Python venv directory
180+
* @throws {Error} If UV installation or venv creation fails
182181
*/
183182
async function installPythonWithUV(destinationDir, pythonVersion = '3.13') {
184-
log('info', `Installing Python ${pythonVersion} using UV`);
183+
log('info', `Creating Python ${pythonVersion} venv using UV`);
185184

186185
// Ensure UV is available, install if necessary
187186
if (!(await isUVAvailable())) {
@@ -195,77 +194,49 @@ async function installPythonWithUV(destinationDir, pythonVersion = '3.13') {
195194
// Ignore cleanup errors (directory might not exist)
196195
}
197196

198-
// Create destination directory structure
199-
await fs.promises.mkdir(destinationDir, { recursive: true });
200-
201197
try {
202-
// Configure environment for UV Python installation
203-
const env = {
204-
...process.env,
205-
UV_PYTHON_INSTALL_DIR: destinationDir,
206-
UV_CACHE_DIR: path.join(core.getTmpDir(), 'uv-cache'),
207-
};
208-
209-
// Execute UV Python installation command
210-
await execFile('uv', ['python', 'install', pythonVersion], {
211-
env,
198+
// Create venv directly using uv venv command with absolute path
199+
const absolutePath = path.resolve(destinationDir);
200+
201+
// Use --python-preference managed to allow UV to download Python if not found on system
202+
await execFile('uv', ['venv', absolutePath, '--python', pythonVersion, '--python-preference', 'managed'], {
212203
timeout: 300000, // 5 minutes timeout for download and installation
213-
cwd: destinationDir,
214204
});
215205

216-
// Verify that Python executable was successfully installed
217-
await ensurePythonExeExists(destinationDir, pythonVersion);
206+
// Verify that Python executable was successfully created
207+
await ensurePythonExeExists(destinationDir);
218208

219-
log('info', `Python ${pythonVersion} installation completed: ${destinationDir}`);
209+
log('info', `Python ${pythonVersion} venv created successfully: ${destinationDir}`);
220210
return destinationDir;
221211
} catch (err) {
222-
throw new Error(`UV Python installation failed: ${err.message}`);
212+
throw new Error(`UV venv creation failed: ${err.message}`);
223213
}
224214
}
225215

226216
/**
227-
* Verify that Python executable exists in the installed directory
228-
* Searches through common installation paths where UV might place Python executables
229-
* @param {string} pythonDir - Directory containing Python installation
230-
* @param {string} pythonVersion - Python version for path construction (default: "3.13")
217+
* Verify that Python executable exists in the venv directory
218+
* Checks the standard venv bin/Scripts directory for Python executable
219+
* @param {string} pythonDir - Directory containing Python venv
231220
* @returns {Promise<boolean>} True if executable exists and is accessible
232221
* @throws {Error} If no Python executable found in expected locations
233222
*/
234-
async function ensurePythonExeExists(pythonDir, pythonVersion = '3.13') {
235-
// UV typically installs to subdirectories organized by version
236-
const possiblePaths = [
237-
pythonDir, // Direct installation in target directory
238-
path.join(pythonDir, 'python'),
239-
path.join(pythonDir, `python-${pythonVersion}`),
240-
path.join(pythonDir, pythonVersion),
241-
];
242-
223+
async function ensurePythonExeExists(pythonDir) {
224+
// Standard venv structure: bin/ on Unix, Scripts/ on Windows
225+
const binDir = proc.IS_WINDOWS
226+
? path.join(pythonDir, 'Scripts')
227+
: path.join(pythonDir, 'bin');
243228
const executables = proc.IS_WINDOWS ? ['python.exe'] : ['python3', 'python'];
244229

245-
for (const basePath of possiblePaths) {
246-
// Check for executable in root of installation path
247-
for (const exeName of executables) {
248-
try {
249-
await fs.promises.access(path.join(basePath, exeName));
250-
return true;
251-
} catch (err) {
252-
// Continue trying other combinations
253-
}
254-
}
255-
256-
// Check for executable in bin subdirectory (Unix-style layout)
257-
const binDir = path.join(basePath, 'bin');
258-
for (const exeName of executables) {
259-
try {
260-
await fs.promises.access(path.join(binDir, exeName));
261-
return true;
262-
} catch (err) {
263-
// Continue trying other combinations
264-
}
230+
for (const exeName of executables) {
231+
try {
232+
await fs.promises.access(path.join(binDir, exeName));
233+
return true;
234+
} catch (err) {
235+
// Continue trying other executables
265236
}
266237
}
267238

268-
throw new Error('Python executable does not exist after UV installation!');
239+
throw new Error('Python executable does not exist after venv creation!');
269240
}
270241

271242
/**
@@ -291,39 +262,31 @@ export async function installPortablePython(destinationDir) {
291262
}
292263

293264
/**
294-
* Locate Python executable in an installed Python directory
295-
* Searches through common locations where UV might install Python executables
296-
* @param {string} pythonDir - Python installation directory to search
265+
* Locate Python executable in a venv directory
266+
* Uses standard venv structure (bin/ on Unix, Scripts/ on Windows)
267+
* @param {string} pythonDir - Python venv directory to search
297268
* @returns {Promise<string>} Full path to Python executable
298-
* @throws {Error} If no executable found in the directory
269+
* @throws {Error} If no executable found in the venv
299270
*/
300271
function getPythonExecutablePath(pythonDir) {
272+
// Standard venv structure
273+
const binDir = proc.IS_WINDOWS
274+
? path.join(pythonDir, 'Scripts')
275+
: path.join(pythonDir, 'bin');
301276
const executables = proc.IS_WINDOWS ? ['python.exe'] : ['python3', 'python'];
302277

303-
// Check common locations where UV might install Python
304-
const searchPaths = [
305-
pythonDir,
306-
path.join(pythonDir, 'bin'),
307-
path.join(pythonDir, 'python'),
308-
path.join(pythonDir, 'python-3.13'),
309-
path.join(pythonDir, '3.13'),
310-
path.join(pythonDir, '3.13', 'bin'),
311-
];
312-
313-
for (const searchPath of searchPaths) {
314-
for (const exeName of executables) {
315-
const fullPath = path.join(searchPath, exeName);
316-
try {
317-
fs.accessSync(fullPath, fs.constants.X_OK);
318-
log('info', `Found Python executable: ${fullPath}`);
319-
return fullPath;
320-
} catch (err) {
321-
// Continue searching through all combinations
322-
}
278+
for (const exeName of executables) {
279+
const fullPath = path.join(binDir, exeName);
280+
try {
281+
fs.accessSync(fullPath, fs.constants.X_OK);
282+
log('info', `Found Python executable: ${fullPath}`);
283+
return fullPath;
284+
} catch (err) {
285+
// Continue searching through all executables
323286
}
324287
}
325288

326-
throw new Error(`Could not find Python executable in ${pythonDir}`);
289+
throw new Error(`Could not find Python executable in venv ${pythonDir}`);
327290
}
328291

329292
// Export utility functions for external use

0 commit comments

Comments
 (0)