]> Git Repo - pico-vscode.git/blob - src/utils/examplesUtil.mts
Merge branch 'main' into main
[pico-vscode.git] / src / utils / examplesUtil.mts
1 import { join as joinPosix } from "path/posix";
2 import Logger, { LoggerSource } from "../logger.mjs";
3 import { existsSync, readFileSync, rmSync } from "fs";
4 import { homedir } from "os";
5 import {
6   getGit,
7   sparseCheckout,
8   sparseCloneRepository,
9   execAsync,
10 } from "./gitUtil.mjs";
11 import Settings from "../settings.mjs";
12 import { checkForGit } from "./requirementsUtil.mjs";
13 import { cp } from "fs/promises";
14 import { get } from "https";
15 import {
16   isInternetConnected,
17   CURRENT_DATA_VERSION,
18   getDataRoot,
19 } from "./downloadHelpers.mjs";
20 import { unknownErrorToString } from "./errorHelper.mjs";
21
22 const EXAMPLES_REPOSITORY_URL =
23   "https://github.com/raspberrypi/pico-examples.git";
24 const EXAMPLES_JSON_URL =
25   "https://raspberrypi.github.io/pico-vscode/" +
26   `${CURRENT_DATA_VERSION}/examples.json`;
27 const EXAMPLES_GITREF = "7fe60d6b4027771e45d97f207532c41b1d8c5418";
28 const EXAMPLES_TAG = "sdk-2.0.0";
29
30 export interface Example {
31   path: string;
32   name: string;
33   libPaths: [string];
34   libNames: [string];
35   boards: [string];
36   supportRiscV: boolean;
37   searchKey: string;
38 }
39
40 interface ExamplesFile {
41   [key: string]: {
42     path: string;
43     name: string;
44     libPaths: [string];
45     libNames: [string];
46     boards: [string];
47     supportRiscV: boolean;
48   };
49 }
50
51 function buildExamplesPath(): string {
52   return joinPosix(homedir().replaceAll("\\", "/"), ".pico-sdk", "examples");
53 }
54
55 function parseExamplesJson(data: string): Example[] {
56   try {
57     const examples = JSON.parse(data.toString()) as ExamplesFile;
58
59     return Object.keys(examples).map(key => ({
60       path: examples[key].path,
61       name: examples[key].name,
62       libPaths: examples[key].libPaths,
63       libNames: examples[key].libNames,
64       boards: examples[key].boards,
65       supportRiscV: examples[key].supportRiscV,
66       searchKey: key,
67     }));
68   } catch (error) {
69     Logger.error(
70       LoggerSource.examples,
71       "Failed to parse examples.json.",
72       unknownErrorToString(error)
73     );
74
75     return [];
76   }
77 }
78
79 /**
80  * Downloads the examples list from the internet or loads the included examples.json.
81  *
82  * @returns A promise that resolves with an array of Example objects.
83  */
84 export async function loadExamples(): Promise<Example[]> {
85   try {
86     if (!(await isInternetConnected())) {
87       throw new Error(
88         "Error while downloading examples list. " + "No internet connection"
89       );
90     }
91     const result = await new Promise<Example[]>((resolve, reject) => {
92       // Download the INI file
93       get(EXAMPLES_JSON_URL, response => {
94         if (response.statusCode !== 200) {
95           reject(
96             new Error(
97               "Error while downloading examples list. " +
98                 `Status code: ${response.statusCode}`
99             )
100           );
101         }
102         let data = "";
103
104         // Append data as it arrives
105         response.on("data", chunk => {
106           data += chunk;
107         });
108
109         // Parse the INI data when the download is complete
110         response.on("end", () => {
111           // Resolve with the array of SupportedToolchainVersion
112           resolve(parseExamplesJson(data));
113         });
114
115         // Handle errors
116         response.on("error", error => {
117           reject(error);
118         });
119       });
120     });
121
122     Logger.info(
123       LoggerSource.examples,
124       "Successfully downloaded examples list from the internet."
125     );
126
127     return result;
128   } catch (error) {
129     Logger.warn(
130       LoggerSource.examples,
131       "Failed to download examples:",
132       unknownErrorToString(error)
133     );
134
135     try {
136       const examplesFile = readFileSync(
137         joinPosix(getDataRoot(), "examples.json")
138       );
139
140       return parseExamplesJson(examplesFile.toString("utf-8"));
141     } catch (error) {
142       Logger.error(
143         LoggerSource.examples,
144         "Failed to load included examples.json.",
145         unknownErrorToString(error)
146       );
147
148       return [];
149     }
150   }
151 }
152
153 export async function setupExample(
154   example: Example,
155   targetPath: string
156 ): Promise<boolean> {
157   const examplesRepoPath = buildExamplesPath();
158   const absoluteExamplePath = joinPosix(examplesRepoPath, example.path);
159
160   const settings = Settings.getInstance();
161   if (settings === undefined) {
162     Logger.log("Error: Settings not initialized.");
163
164     return false;
165   }
166
167   // TODO: this does take about 2s - may be reduced
168   const requirementsCheck = await checkForGit(settings);
169   if (!requirementsCheck) {
170     return false;
171   }
172
173   const gitPath = await getGit(settings);
174
175   if (existsSync(joinPosix(examplesRepoPath, ".git"))) {
176     const ref = await execAsync(
177       `cd ${
178         process.env.ComSpec?.endsWith("cmd.exe") ? "/d " : " "
179       }"${examplesRepoPath}" && ${
180         process.env.ComSpec === "powershell.exe" ? "&" : ""
181       }"${gitPath}" rev-parse HEAD`
182     );
183     Logger.log(`Examples git ref is ${ref.stdout}\n`);
184     if (ref.stdout.trim() !== EXAMPLES_GITREF) {
185       Logger.log(`Removing old examples repo\n`);
186       rmSync(examplesRepoPath, { recursive: true, force: true });
187     }
188   }
189
190   if (!existsSync(examplesRepoPath)) {
191     const result = await sparseCloneRepository(
192       EXAMPLES_REPOSITORY_URL,
193       EXAMPLES_TAG,
194       examplesRepoPath,
195       gitPath
196     );
197
198     if (!result) {
199       return result;
200     }
201   }
202
203   Logger.log(`Sparse-checkout selected example: ${example.name}`);
204   const result = await sparseCheckout(examplesRepoPath, example.path, gitPath);
205   if (!result) {
206     return result;
207   }
208
209   for (const libPath of example.libPaths) {
210     Logger.log(`Sparse-checkout selected example required path: ${libPath}`);
211     const result = await sparseCheckout(examplesRepoPath, libPath, gitPath);
212     if (!result) {
213       return result;
214     }
215   }
216
217   Logger.log(`Copying example from ${absoluteExamplePath} to ${targetPath}`);
218   // TODO: use example.name or example.search key for project folder name?
219   await cp(absoluteExamplePath, joinPosix(targetPath, example.searchKey), {
220     recursive: true,
221   });
222
223   for (let i = 0; i < example.libPaths.length; i++) {
224     const libPath = example.libPaths[i];
225     const libName = example.libNames[i];
226     const absoluteLibPath = joinPosix(examplesRepoPath, libPath);
227     Logger.log(
228       `Copying example required path from ${absoluteLibPath} ` +
229         `to ${targetPath}/${example.searchKey}/${libName}`
230     );
231     // TODO: use example.name or example.search key for project folder name?
232     await cp(
233       absoluteLibPath,
234       joinPosix(targetPath, example.searchKey, libName),
235       { recursive: true }
236     );
237   }
238
239   Logger.log("Done copying example.");
240
241   return result;
242 }
This page took 0.034173 seconds and 4 git commands to generate.