Skip to content

Commit 7dcf9c7

Browse files
authored
simplify and harden venv install in folder penv
2 parents aa06861 + 4212973 commit 7dcf9c7

File tree

4 files changed

+63
-89
lines changed

4 files changed

+63
-89
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: 60 additions & 86 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,60 @@ 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,
212-
timeout: 300000, // 5 minutes timeout for download and installation
213-
cwd: destinationDir,
214-
});
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(
203+
'uv',
204+
[
205+
'venv',
206+
absolutePath,
207+
'--python',
208+
pythonVersion,
209+
'--python-preference',
210+
'managed',
211+
],
212+
{
213+
timeout: 300000, // 5 minutes timeout for download and installation
214+
},
215+
);
215216

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

219-
log('info', `Python ${pythonVersion} installation completed: ${destinationDir}`);
220+
log('info', `Python ${pythonVersion} venv created successfully: ${destinationDir}`);
220221
return destinationDir;
221222
} catch (err) {
222-
throw new Error(`UV Python installation failed: ${err.message}`);
223+
throw new Error(`UV venv creation failed: ${err.message}`);
223224
}
224225
}
225226

226227
/**
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")
228+
* Verify that Python executable exists in the venv directory
229+
* Checks the standard venv bin/Scripts directory for Python executable
230+
* @param {string} pythonDir - Directory containing Python venv
231231
* @returns {Promise<boolean>} True if executable exists and is accessible
232232
* @throws {Error} If no Python executable found in expected locations
233233
*/
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-
234+
async function ensurePythonExeExists(pythonDir) {
235+
// Standard venv structure: bin/ on Unix, Scripts/ on Windows
236+
const binDir = proc.IS_WINDOWS
237+
? path.join(pythonDir, 'Scripts')
238+
: path.join(pythonDir, 'bin');
243239
const executables = proc.IS_WINDOWS ? ['python.exe'] : ['python3', 'python'];
244240

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-
}
241+
for (const exeName of executables) {
242+
try {
243+
await fs.promises.access(path.join(binDir, exeName));
244+
return true;
245+
} catch (err) {
246+
// Continue trying other executables
265247
}
266248
}
267249

268-
throw new Error('Python executable does not exist after UV installation!');
250+
throw new Error('Python executable does not exist after venv creation!');
269251
}
270252

271253
/**
@@ -291,39 +273,31 @@ export async function installPortablePython(destinationDir) {
291273
}
292274

293275
/**
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
276+
* Locate Python executable in a venv directory
277+
* Uses standard venv structure (bin/ on Unix, Scripts/ on Windows)
278+
* @param {string} pythonDir - Python venv directory to search
297279
* @returns {Promise<string>} Full path to Python executable
298-
* @throws {Error} If no executable found in the directory
280+
* @throws {Error} If no executable found in the venv
299281
*/
300282
function getPythonExecutablePath(pythonDir) {
283+
// Standard venv structure
284+
const binDir = proc.IS_WINDOWS
285+
? path.join(pythonDir, 'Scripts')
286+
: path.join(pythonDir, 'bin');
301287
const executables = proc.IS_WINDOWS ? ['python.exe'] : ['python3', 'python'];
302288

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-
}
289+
for (const exeName of executables) {
290+
const fullPath = path.join(binDir, exeName);
291+
try {
292+
fs.accessSync(fullPath, fs.constants.X_OK);
293+
log('info', `Found Python executable: ${fullPath}`);
294+
return fullPath;
295+
} catch (err) {
296+
// Continue searching through all executables
323297
}
324298
}
325299

326-
throw new Error(`Could not find Python executable in ${pythonDir}`);
300+
throw new Error(`Could not find Python executable in venv ${pythonDir}`);
327301
}
328302

329303
// Export utility functions for external use

0 commit comments

Comments
 (0)