]> Git Repo - pico-vscode.git/commitdiff
macOS pyenv for Python installation
authorpaulober <[email protected]>
Sat, 16 Dec 2023 00:43:34 +0000 (01:43 +0100)
committerpaulober <[email protected]>
Sat, 16 Dec 2023 00:43:34 +0000 (01:43 +0100)
Signed-off-by: paulober <[email protected]>
README.md
scripts/pico_project.py
src/utils/cmakeUtil.mts
src/utils/download.mts
src/utils/gitUtil.mts
src/utils/githubREST.mts
src/utils/pyenvUtil.mts [new file with mode: 0644]
src/webview/newProjectPanel.mts

index 727c60d83c74c641036ed5570af494bf02e33885..3e6c939a5729a6d7b7b8baee23ff685477a8cf1c 100644 (file)
--- a/README.md
+++ b/README.md
@@ -41,5 +41,5 @@ This extension contributes the following settings:
 
 ## Known Issues
 
-- If the extension installs Python it can't currently be used due to an issue with dynlib include paths (macOS only)
 - Cannot run project generator 2 times in a row without restarting the extension
+- Custom Ninja, Python3 or git paths are not stored in CMakeLists.txt like SDK and Toolchain paths so using them would require to build and configure the project thought the extension
index a4cc03489d9de5fae49f3885b13cb0add1d75f28..c784395ff28fdf521b808d20b1f0451b38fd60af 100644 (file)
@@ -958,6 +958,7 @@ def ParseCommandLine():
     parser.add_argument("-tcVersion", "--toolchainVersion", help="ARM Embeded Toolchain version to use (required)")
     parser.add_argument("-np", "--ninjaPath", help="Ninja path")
     parser.add_argument("-cmp", "--cmakePath", help="CMake path")
+    parser.add_argument("-cupy", "--customPython", action='store_true', help="Custom python path used to execute the script.")
 
     return parser.parse_args()
 
@@ -1165,7 +1166,7 @@ def GenerateCMake(folder, params):
 
 
 # Generates the requested project files, if any
-def generateProjectFiles(projectPath, projectName, sdkPath, projects, debugger, sdkVersion, toolchainVersion, ninjaPath, cmakePath):
+def generateProjectFiles(projectPath, projectName, sdkPath, projects, debugger, sdkVersion, toolchainVersion, ninjaPath, cmakePath, customPython):
 
     oldCWD = os.getcwd()
 
@@ -1286,9 +1287,13 @@ def generateProjectFiles(projectPath, projectName, sdkPath, projects, debugger,
     "cmake.cmakePath": "{cmakePath}",
     "raspberry-pi-pico.cmakeAutoConfigure": true,
     "raspberry-pi-pico.cmakePath": "{cmakePath.replace(user_home, "${HOME}") if use_home_var else cmakePath}",
-    "raspberry-pi-pico.ninjaPath": "{ninjaPath.replace(user_home, "${HOME}") if use_home_var else ninjaPath}"
-}}
-'''
+    "raspberry-pi-pico.ninjaPath": "{ninjaPath.replace(user_home, "${HOME}") if use_home_var else ninjaPath}"'''
+
+            if customPython:
+                settings += f''',
+    "raspberry-pi-pico.python3Path": "{sys.executable.replace(user_home, "${HOME}") if use_home_var else sys.executable}"'''
+                
+            settings += '\n}\n'
 
             # extensions
             extensions = f'''{{
@@ -1493,7 +1498,8 @@ def DoEverything(parent, params):
             params["sdkVersion"], 
             params["toolchainVersion"], 
             params["ninjaPath"], 
-            params["cmakePath"])
+            params["cmakePath"],
+            params["customPython"])
 
     if params['wantBuild']:
         if params['wantGUI'] and ENABLE_TK_GUI:
@@ -1610,6 +1616,7 @@ else :
         'toolchainVersion': args.toolchainVersion,
         'ninjaPath'     : args.ninjaPath,
         'cmakePath'     : args.cmakePath,
+        'customPython'  : args.customPython
         }
 
     DoEverything(None, params)
index 6cb98b7d96e457a07af5f7189d30701efb056bf8..aaec2f8e5daa00075cf6a03aca7f72c90d4320d3 100644 (file)
@@ -1,7 +1,7 @@
 import { exec } from "child_process";
 import { workspace, type Uri, window, ProgressLocation } from "vscode";
 import { showRequirementsNotMetErrorMessage } from "./requirementsUtil.mjs";
-import { dirname, join, resolve } from "path";
+import { dirname, join } from "path";
 import type Settings from "../settings.mjs";
 import { HOME_VAR, SettingsKey } from "../settings.mjs";
 import { readFileSync } from "fs";
@@ -31,13 +31,15 @@ export async function configureCmakeNinja(
         "cmake",
       { nothrow: true }
     );
+    console.log(
+      settings.getString(SettingsKey.python3Path)?.replace(HOME_VAR, homedir())
+    );
     // TODO: maybe also check for "python" on unix systems
     const pythonPath = await which(
       settings
         .getString(SettingsKey.python3Path)
-        ?.replace(HOME_VAR, homedir()) || process.platform === "win32"
-        ? "python"
-        : "python3",
+        ?.replace(HOME_VAR, homedir()) ||
+        (process.platform === "win32" ? "python" : "python3"),
       { nothrow: true }
     );
 
@@ -75,9 +77,9 @@ export async function configureCmakeNinja(
         // TODO: maybe delete the build folder before running cmake so
         // all configuration files in build get updates
         const customEnv = process.env;
-        customEnv["PYTHONHOME"] = pythonPath.includes("/")
+        /*customEnv["PYTHONHOME"] = pythonPath.includes("/")
           ? resolve(join(dirname(pythonPath), ".."))
-          : "";
+          : "";*/
         const isWindows = process.platform === "win32";
         customEnv[isWindows ? "Path" : "PATH"] = `${
           ninjaPath.includes("/") ? dirname(ninjaPath) : ""
@@ -94,8 +96,11 @@ export async function configureCmakeNinja(
         const child = exec(
           `${
             process.platform === "win32" ? "&" : ""
-          }"${cmake}" -DCMAKE_BUILD_TYPE=Debug ` +
-            `-G Ninja -B ./build "${folder.fsPath}"`,
+          }"${cmake}" -DCMAKE_BUILD_TYPE=Debug ${
+            pythonPath.includes("/")
+              ? `-DPython3_EXECUTABLE="${pythonPath.replaceAll("\\", "/")}" `
+              : ""
+          }` + `-G Ninja -B ./build "${folder.fsPath}"`,
           {
             env: customEnv,
             cwd: folder.fsPath,
index ee9397941a5f241a2034763da603c199df862337..1faa7577cc9040eb33d94636c99b035a73bb4b5c 100644 (file)
@@ -641,7 +641,10 @@ export async function downloadEmbedPython(
   redirectURL?: string
 ): Promise<string | undefined> {
   if (
-    process.platform === "linux" ||
+    // even tough this function supports downloading python3 on macOS arm64
+    // it doesn't work correctly therefore it's excluded here
+    // use pyenvInstallPython instead
+    process.platform !== "win32" ||
     (process.platform === "win32" && process.arch !== "x64")
   ) {
     Logger.log(
@@ -665,20 +668,14 @@ export async function downloadEmbedPython(
   // Ensure the target directory exists
   await mkdir(targetDirectory, { recursive: true });
 
-  // select download url for platform()_arch()
-  const downloadUrl =
-    redirectURL ??
-    (process.platform === "darwin"
-      ? versionBundle.python.macos
-      : versionBundle.python.windowsAmd64);
+  // select download url
+  const downloadUrl = versionBundle.python.windowsAmd64;
 
   const tmpBasePath = join(tmpdir(), "pico-sdk");
   await mkdir(tmpBasePath, { recursive: true });
   const archiveFilePath = join(
     tmpBasePath,
-    `python-${versionBundle.python.version}.${
-      process.platform === "darwin" ? "pkg" : "zip"
-    }`
+    `python-${versionBundle.python.version}.zip`
   );
 
   return new Promise(resolve => {
@@ -712,6 +709,8 @@ export async function downloadEmbedPython(
 
       // save the file to disk
       const fileWriter = createWriteStream(archiveFilePath).on("finish", () => {
+        // doesn't work correctly therefore use pyenvInstallPython instead
+        // TODO: remove unused darwin code-path here
         if (process.platform === "darwin") {
           const pkgExtractor = new MacOSPythonPkgExtractor(
             archiveFilePath,
@@ -732,21 +731,28 @@ export async function downloadEmbedPython(
               if (success) {
                 try {
                   // create symlink, so the same path can be used as on Windows
-                  symlinkSync(
-                    joinPosix(
-                      settingsTargetDirectory,
-                      "/Versions/",
-                      versionBundle.python.version.substring(
-                        0,
-                        versionBundle.python.version.lastIndexOf(".")
-                      ),
-                      "bin",
-                      "python3"
+                  const srcPath = joinPosix(
+                    settingsTargetDirectory,
+                    "/Versions/",
+                    versionBundle.python.version.substring(
+                      0,
+                      versionBundle.python.version.lastIndexOf(".")
                     ),
+                    "bin",
+                    "python3"
+                  );
+                  symlinkSync(
+                    srcPath,
                     // use .exe as python is already used in the directory
                     join(settingsTargetDirectory, "python.exe"),
                     "file"
                   );
+                  symlinkSync(
+                    srcPath,
+                    // use .exe as python is already used in the directory
+                    join(settingsTargetDirectory, "python3.exe"),
+                    "file"
+                  );
                 } catch {
                   resolve(undefined);
                 }
index 6722b088ad55462521185217e845716b643594e1..e0dc70adf69b4392f19a1db58644d2874c003106 100644 (file)
@@ -48,14 +48,21 @@ export async function cloneRepository(
   try {
     await execAsync(cloneCommand);
 
-    Logger.log(`SDK ${branch} has been cloned and installed.`);
+    Logger.log(`SDK/Pyenv ${branch} has been cloned and installed.`);
 
     return true;
   } catch (error) {
-    await unlink(targetDirectory);
+    try {
+      await unlink(targetDirectory);
+    } catch {
+      /* */
+    }
 
     const err = error instanceof Error ? error.message : (error as string);
-    Logger.log(`Error while cloning SDK: ${err}`);
+    if (err.includes("already exists")) {
+      return true;
+    }
+    Logger.log(`Error while cloning repository: ${err}`);
 
     return false;
   }
index 861d404a576b0adb970d676f416e9828e646a3bf..ab57820b5a0b903e7cbf5abe59cb68cd085312da 100644 (file)
@@ -10,6 +10,7 @@ interface GithubRelease {
 export const SDK_REPOSITORY_URL = "https://github.com/raspberrypi/pico-sdk.git";
 export const NINJA_REPOSITORY_URL = "https://github.com/ninja-build/ninja.git";
 export const CMAKE_REPOSITORY_URL = "https://github.com/Kitware/CMake.git";
+export const PYENV_REPOSITORY_URL = "https://github.com/pyenv/pyenv.git";
 
 export async function getSDKReleases(): Promise<GithubRelease[]> {
   const octokit = new Octokit();
diff --git a/src/utils/pyenvUtil.mts b/src/utils/pyenvUtil.mts
new file mode 100644 (file)
index 0000000..c897f76
--- /dev/null
@@ -0,0 +1,80 @@
+import { homedir } from "os";
+import { cloneRepository } from "./gitUtil.mjs";
+import { PYENV_REPOSITORY_URL } from "./githubREST.mjs";
+import { join as joinPosix } from "path/posix";
+import { exec } from "child_process";
+import { buildPython3Path } from "./download.mjs";
+import { HOME_VAR } from "../settings.mjs";
+import { existsSync, mkdirSync, symlinkSync } from "fs";
+import { join } from "path";
+
+export function buildPyEnvPath(): string {
+  // TODO: maybe replace . with _
+  return joinPosix(homedir().replaceAll("\\", "/"), ".pico-sdk", "pyenv");
+}
+
+/**
+ * Download pyenv and install it.
+ */
+export async function setupPyenv(): Promise<boolean> {
+  const targetDirectory = buildPyEnvPath();
+  const result = await cloneRepository(
+    PYENV_REPOSITORY_URL,
+    "master",
+    targetDirectory
+  );
+
+  if (!result) {
+    return false;
+  }
+
+  return true;
+}
+
+export async function pyenvInstallPython(
+  version: string
+): Promise<string | null> {
+  const targetDirectory = buildPyEnvPath();
+  const binDirectory = joinPosix(targetDirectory, "bin");
+  const command = `${binDirectory}/pyenv install ${version}`;
+
+  const customEnv = { ...process.env };
+  customEnv["PYENV_ROOT"] = targetDirectory;
+  customEnv[
+    process.platform === "win32" ? "Path" : "PATH"
+  ] = `${binDirectory};${customEnv["PATH"]}`;
+
+  const settingsTarget =
+    `${HOME_VAR}/.pico-sdk` + `/python/${version}/python.exe`;
+  const pythonVersionPath = buildPython3Path(version);
+
+  if (existsSync(pythonVersionPath)) {
+    return settingsTarget;
+  }
+
+  return new Promise(resolve => {
+    exec(
+      command,
+      { env: customEnv, cwd: binDirectory },
+      (error, stdout, stderr) => {
+        if (error) {
+          resolve(null);
+        }
+
+        const versionFolder = joinPosix(targetDirectory, "versions", version);
+        const pyBin = joinPosix(versionFolder, "bin");
+        mkdirSync(pythonVersionPath, { recursive: true });
+        symlinkSync(
+          joinPosix(pyBin, "python3"),
+          joinPosix(pythonVersionPath, "python.exe")
+        );
+        symlinkSync(
+          joinPosix(pyBin, "python3"),
+          joinPosix(pythonVersionPath, "python3exe")
+        );
+
+        resolve(settingsTarget);
+      }
+    );
+  });
+}
index 4cee874d991db5bade5914a2909e9a48635b62e0..aa649ec69071773be3491fa305606afb6b9a837a 100644 (file)
@@ -12,6 +12,7 @@ import {
   commands,
   ColorThemeKind,
   ProgressLocation,
+  Location,
 } from "vscode";
 import { type ExecOptions, exec } from "child_process";
 import { HOME_VAR } from "../settings.mjs";
@@ -48,6 +49,7 @@ import VersionBundlesLoader, {
 import which from "which";
 import { homedir } from "os";
 import { symlink } from "fs/promises";
+import { pyenvInstallPython, setupPyenv } from "../utils/pyenvUtil.mjs";
 
 interface SubmitMessageValue {
   projectName: string;
@@ -422,11 +424,52 @@ export class NewProjectPanel {
                   process.platform === "win32"
                 ) {
                   switch (data.pythonMode) {
-                    case 0:
-                      python3Path = await downloadEmbedPython(
-                        this._versionBundle
+                    case 0: {
+                      const versionBundle = this._versionBundle;
+                      await window.withProgress(
+                        {
+                          location: ProgressLocation.Notification,
+                          title:
+                            "Download and installing Python. This may take a while...",
+                          cancellable: false,
+                        },
+                        async progress => {
+                          if (process.platform === "win32") {
+                            python3Path = await downloadEmbedPython(
+                              versionBundle
+                            );
+                          } else if (process.platform === "darwin") {
+                            const result1 = await setupPyenv();
+                            if (!result1) {
+                              progress.report({
+                                increment: 100,
+                              });
+
+                              return;
+                            }
+                            const result = await pyenvInstallPython(
+                              versionBundle.python.version
+                            );
+
+                            if (result !== null) {
+                              python3Path = result;
+                            }
+                          } else {
+                            this._logger.error(
+                              "Automatic python installation is only supported on Windows and macOS."
+                            );
+
+                            await window.showErrorMessage(
+                              "Automatic python installation is only supported on Windows and macOS."
+                            );
+                          }
+                          progress.report({
+                            increment: 100,
+                          });
+                        }
                       );
                       break;
+                    }
                     case 1:
                       python3Path =
                         process.platform === "win32" ? "python" : "python3";
@@ -1269,9 +1312,9 @@ export class NewProjectPanel {
     };
     // add compiler to PATH
     const isWindows = process.platform === "win32";
-    customEnv["PYTHONHOME"] = pythonExe.includes("/")
+    /*customEnv["PYTHONHOME"] = pythonExe.includes("/")
       ? resolve(join(dirname(pythonExe), ".."))
-      : "";
+      : "";*/
     customEnv[isWindows ? "Path" : "PATH"] = `${join(
       options.toolchainAndSDK.toolchainPath,
       "bin"
@@ -1283,10 +1326,6 @@ export class NewProjectPanel {
       options.ninjaExecutable.includes("/")
         ? `${isWindows ? ";" : ":"}${dirname(options.ninjaExecutable)}`
         : ""
-    }${
-      pythonExe.includes("/")
-        ? `${isWindows ? ";" : ":"}${dirname(pythonExe)}`
-        : ""
     }${isWindows ? ";" : ":"}${customEnv[isWindows ? "Path" : "PATH"]}`;
 
     const command: string = [
@@ -1313,7 +1352,8 @@ export class NewProjectPanel {
       `"${options.ninjaExecutable}"`,
       "--cmakePath",
       `"${options.cmakeExecutable}"`,
-      `"${options.name}"`,
+      // set custom python executable path used flag if python executable is not in PATH
+      pythonExe.includes("/") ? `-cupy "${options.name}"` : `"${options.name}"`,
     ].join(" ");
 
     this._logger.debug(`Executing project generator command: ${command}`);
This page took 0.048091 seconds and 4 git commands to generate.