From 478d956dd184f2894033d06f500a2b353459cb81 Mon Sep 17 00:00:00 2001 From: William Vinnicombe Date: Mon, 6 Jul 2026 16:18:13 +0100 Subject: [PATCH] Attempt to fix issue updating examples --- src/utils/examplesUtil.mts | 116 +++++++++++++++++++++++++++++++------ src/utils/gitUtil.mts | 6 +- 2 files changed, 100 insertions(+), 22 deletions(-) diff --git a/src/utils/examplesUtil.mts b/src/utils/examplesUtil.mts index 7efd5745..67d89082 100644 --- a/src/utils/examplesUtil.mts +++ b/src/utils/examplesUtil.mts @@ -1,6 +1,13 @@ import { join as joinPosix } from "path/posix"; import Logger, { LoggerSource } from "../logger.mjs"; -import { existsSync, readFileSync, rmSync } from "fs"; +import { + chmodSync, + existsSync, + lstatSync, + readdirSync, + readFileSync, + rmSync, +} from "fs"; import { homedir } from "os"; import { ensureGit, @@ -46,6 +53,62 @@ function buildExamplesPath(): string { return joinPosix(homedir().replaceAll("\\", "/"), ".pico-sdk", "examples"); } +/** + * Recursively clears the read-only attribute on a file or directory tree. + * Git for Windows marks some files as read-only, which causes fs.rmSync + * to throw EPERM instead of deleting them, even with { force: true }. + */ +function clearReadOnlyRecursive(targetPath: string): void { + let stat; + try { + stat = lstatSync(targetPath); + } catch { + return; + } + + // Skip symlinks - do not follow them, and their target's permissions + // are irrelevant to removing the link itself. + if (stat.isSymbolicLink()) { + return; + } + + if (stat.isDirectory()) { + try { + for (const entry of readdirSync(targetPath)) { + clearReadOnlyRecursive(joinPosix(targetPath, entry)); + } + } catch { + /* best effort */ + } + } + + try { + chmodSync(targetPath, stat.isDirectory() ? 0o777 : 0o666); + } catch { + /* best effort */ + } +} + +/** + * Removes the examples repository directory. + * + * @returns True if the directory does not exist anymore, false otherwise. + */ +function removeExamplesRepo(examplesRepoPath: string): boolean { + clearReadOnlyRecursive(examplesRepoPath); + try { + rmSync(examplesRepoPath, { recursive: true, force: true }); + } catch (error) { + Logger.warn( + LoggerSource.examples, + "Failed to remove examples repo.", + unknownErrorToString(error) + ); + } + + return !existsSync(examplesRepoPath); +} + function parseExamplesJson(data: string): Example[] { try { const examples = JSON.parse(data.toString()) as ExamplesFile; @@ -195,6 +258,7 @@ export async function setupExample( } if (existsSync(joinPosix(examplesRepoPath, ".git"))) { + let needsRemoval: boolean; try { const ref = await execAsync( `cd ${ @@ -207,35 +271,49 @@ export async function setupExample( LoggerSource.examples, `Examples git ref is ${ref.stdout}\n` ); - if (ref.stdout.trim() !== EXAMPLES_GITREF) { - Logger.debug(LoggerSource.examples, `Removing old examples repo\n`); - rmSync(examplesRepoPath, { recursive: true, force: true }); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars + needsRemoval = ref.stdout.trim() !== EXAMPLES_GITREF; } catch (error) { // Corrupted examples directory - Logger.debug(LoggerSource.examples, `Removing corrupted examples repo\n`); - try { - rmSync(examplesRepoPath, { recursive: true, force: true }); - } catch (error) { - Logger.warn( + Logger.debug( + LoggerSource.examples, + "Failed to read examples git ref, treating repo as corrupted.", + unknownErrorToString(error) + ); + needsRemoval = true; + } + + if (needsRemoval) { + Logger.debug(LoggerSource.examples, `Removing old examples repo\n`); + if (!removeExamplesRepo(examplesRepoPath)) { + Logger.error( LoggerSource.examples, - "Failed to remove corrupted examples repo.", - unknownErrorToString(error) + `Failed to remove outdated/corrupted examples repo at ` + + `${examplesRepoPath}.` ); + void window.showErrorMessage( + "Failed to update the examples repository - it may be in use " + + `by another process. Please close any programs that might be ` + + `using files in ${examplesRepoPath} and try again, or delete ` + + "this folder manually." + ); + + return false; } } } else if (existsSync(examplesRepoPath)) { // Examples path exists, but does not contain a git repository Logger.debug(LoggerSource.examples, `Removing empty examples repo\n`); - try { - rmSync(examplesRepoPath, { recursive: true, force: true }); - } catch (error) { - Logger.warn( + if (!removeExamplesRepo(examplesRepoPath)) { + Logger.error( LoggerSource.examples, - "Failed to remove empty examples repo.", - unknownErrorToString(error) + `Failed to remove invalid examples repo at ${examplesRepoPath}.` + ); + void window.showErrorMessage( + `Failed to prepare the examples repository. Please delete ` + + `${examplesRepoPath} and try again.` ); + + return false; } } diff --git a/src/utils/gitUtil.mts b/src/utils/gitUtil.mts index bf63dbb6..1b3a7090 100644 --- a/src/utils/gitUtil.mts +++ b/src/utils/gitUtil.mts @@ -1,7 +1,7 @@ import { promisify } from "util"; import { exec } from "child_process"; import Logger, { LoggerSource } from "../logger.mjs"; -import { unlink } from "fs/promises"; +import { rm } from "fs/promises"; import Settings from "../settings.mjs"; import { SettingsKey, HOME_VAR } from "../settings.mjs"; import { homedir } from "os"; @@ -193,7 +193,7 @@ export async function cloneRepository( return true; } catch (error) { try { - await unlink(targetDirectory); + await rm(targetDirectory, { recursive: true, force: true }); } catch { /* */ } @@ -239,7 +239,7 @@ export async function sparseCloneRepository( return true; } catch (error) { try { - await unlink(targetDirectory); + await rm(targetDirectory, { recursive: true, force: true }); } catch { /* */ }