Skip to content

Commit bc4d945

Browse files
committed
Download portable Python from PlatformIO Trusted Registry
1 parent 87622ea commit bc4d945

File tree

5 files changed

+178
-178
lines changed

5 files changed

+178
-178
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,12 @@
5050
"dependencies": {
5151
"cross-spawn": "~7.0.3",
5252
"glob": "~7.1.6",
53+
"global-agent": "^2.1.12",
54+
"got": "^11.8.2",
5355
"jsonrpc-lite": "~2.1.1",
5456
"os-tmpdir": "*",
5557
"querystringify": "*",
56-
"request": "~2.88.2",
58+
"semver": "^7.3.4",
5759
"tar": "~6.1.0",
5860
"tcp-port-used": "~1.0.2",
5961
"tmp": "~0.2.1",

src/installer/get-python.js

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/**
2+
* Copyright (c) 2017-present PlatformIO <[email protected]>
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the license found in the LICENSE file in
6+
* the root directory of this source tree.
7+
*/
8+
9+
import * as core from '../core';
10+
import * as proc from '../proc';
11+
12+
import crypto from 'crypto';
13+
import fs from 'fs';
14+
import got from 'got';
15+
import path from 'path';
16+
import { promisify } from 'util';
17+
import semver from 'semver';
18+
import stream from 'stream';
19+
import tar from 'tar';
20+
import zlib from 'zlib';
21+
22+
export async function installPortablePython(destinationDir) {
23+
const registryFile = await getRegistryFile();
24+
if (!registryFile) {
25+
throw new Error(`Could not find portable Python for ${proc.getSysType()}`);
26+
}
27+
const archivePath = await downloadRegistryFile(registryFile, core.getTmpDir());
28+
if (!archivePath) {
29+
throw new Error('Could not download portable Python');
30+
}
31+
await extractTarGz(archivePath, destinationDir);
32+
await ensurePythonExeExists(destinationDir);
33+
return destinationDir;
34+
}
35+
36+
async function getRegistryFile() {
37+
const systype = proc.getSysType();
38+
const response = await got(
39+
'https://api.registry.platformio.org/v3/packages/platformio/tool/python-portable',
40+
{ timeout: 60 * 1000, retry: { limit: 5 } }
41+
).json();
42+
const versions = response.versions.filter((version) =>
43+
isVersionSystemCompatible(version, systype)
44+
);
45+
let bestVersion = undefined;
46+
for (const version of versions) {
47+
if (!bestVersion || semver.gt(version.name, bestVersion.name)) {
48+
bestVersion = version;
49+
}
50+
}
51+
if (!bestVersion) {
52+
return;
53+
}
54+
return bestVersion.files.find((item) => item.system.includes(systype));
55+
}
56+
57+
function isVersionSystemCompatible(version, systype) {
58+
for (const item of version.files) {
59+
if (item.system.includes(systype)) {
60+
return true;
61+
}
62+
}
63+
return false;
64+
}
65+
66+
async function downloadRegistryFile(regfile, destinationDir) {
67+
for await (const { url, checksum } of registryFileMirrorIterator(
68+
regfile.download_url
69+
)) {
70+
const archivePath = path.join(destinationDir, regfile.name);
71+
// if already downloaded
72+
if (await fileExistsAndChecksumMatches(archivePath, checksum)) {
73+
return archivePath;
74+
}
75+
const pipeline = promisify(stream.pipeline);
76+
await pipeline(got.stream(url), fs.createWriteStream(archivePath));
77+
if (await fileExistsAndChecksumMatches(archivePath, checksum)) {
78+
return archivePath;
79+
}
80+
}
81+
}
82+
83+
async function* registryFileMirrorIterator(downloadUrl) {
84+
const visitedMirrors = [];
85+
while (true) {
86+
const response = await got.head(downloadUrl, {
87+
followRedirect: false,
88+
throwHttpErrors: false,
89+
timeout: 60 * 1000,
90+
retry: { limit: 5 },
91+
searchParams: visitedMirrors.length
92+
? { bypass: visitedMirrors.join(',') }
93+
: undefined,
94+
});
95+
const stopConditions = [
96+
![302, 307].includes(response.statusCode),
97+
!response.headers.location,
98+
!response.headers['x-pio-mirror'],
99+
visitedMirrors.includes(response.headers['x-pio-mirror']),
100+
];
101+
if (stopConditions.some((cond) => cond)) {
102+
return;
103+
}
104+
visitedMirrors.push(response.headers['x-pio-mirror']);
105+
yield {
106+
url: response.headers.location,
107+
checksum: response.headers['x-pio-content-sha256'],
108+
};
109+
}
110+
}
111+
112+
async function fileExistsAndChecksumMatches(filePath, checksum) {
113+
try {
114+
await fs.promises.access(filePath);
115+
if ((await calculateFileHashsum(filePath)) === checksum) {
116+
return true;
117+
}
118+
await fs.promises.unlink(filePath);
119+
} catch (err) {}
120+
return false;
121+
}
122+
123+
async function calculateFileHashsum(filePath, algo = 'sha256') {
124+
return new Promise((resolve, reject) => {
125+
const hash = crypto.createHash(algo);
126+
const fsStream = fs.createReadStream(filePath);
127+
fsStream.on('data', (data) => hash.update(data));
128+
fsStream.on('end', () => resolve(hash.digest('hex')));
129+
fsStream.on('error', (err) => reject(err));
130+
});
131+
}
132+
133+
async function extractTarGz(source, destination) {
134+
try {
135+
await fs.promises.access(destination);
136+
} catch (err) {
137+
await fs.promises.mkdir(destination, { recursive: true });
138+
}
139+
return await new Promise((resolve, reject) => {
140+
fs.createReadStream(source)
141+
.pipe(zlib.createGunzip())
142+
.on('error', (err) => reject(err))
143+
.pipe(
144+
tar.extract({
145+
cwd: destination,
146+
})
147+
)
148+
.on('error', (err) => reject(err))
149+
.on('close', () => resolve(destination));
150+
});
151+
}
152+
153+
async function ensurePythonExeExists(pythonDir) {
154+
const binDir = proc.IS_WINDOWS ? pythonDir : path.join(pythonDir, 'bin');
155+
for (const name of ['python.exe', 'python3', 'python']) {
156+
try {
157+
await fs.promises.access(path.join(binDir, name));
158+
return true;
159+
} catch (err) {}
160+
}
161+
throw new Error('Python executable does not exist!');
162+
}

src/installer/helpers.js

Lines changed: 0 additions & 120 deletions
This file was deleted.

0 commit comments

Comments
 (0)