Skip to content

Commit 453cc8a

Browse files
committed
finish installation UI
1 parent fba25ae commit 453cc8a

File tree

2 files changed

+226
-18
lines changed

2 files changed

+226
-18
lines changed

src/compilers.ts

Lines changed: 220 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as path from "path";
55
import * as ChildProcess from "child_process";
66
import { config } from './extension';
77
import { determineOutputFolder, downloadFileInteractive } from './installer';
8+
import { reqText } from './util';
89

910
export interface DetectedCompiler {
1011
/**
@@ -31,6 +32,9 @@ type UIQuickPickItem = vscode.QuickPickItem & { kind?: number, installInfo?: any
3132
export async function setupCompilersUI() {
3233
const introQuickPick = vscode.window.createQuickPick();
3334
introQuickPick.title = "Setup auto-detected compiler or manually configure compiler";
35+
introQuickPick.busy = true;
36+
introQuickPick.items = [{ label: "Detecting compilers..." }];
37+
introQuickPick.show();
3438
const compilers: DetectedCompiler[] = await listCompilers();
3539
let items: UIQuickPickItem[] = [];
3640
for (let i = 0; i < compilers.length; i++) {
@@ -52,6 +56,8 @@ export async function setupCompilersUI() {
5256
}
5357
if (compiler.frontendVersion && compiler.frontendVersion != compiler.version)
5458
versionStrings.push("spec version " + compiler.frontendVersion);
59+
if (!compiler.inPath && compiler.path)
60+
versionStrings.push(compiler.path);
5561
items.push({
5662
label: compiler.has,
5763
description: versionStrings.length > 0 ? versionStrings.join(" ・ ") : undefined,
@@ -82,13 +88,13 @@ export async function setupCompilersUI() {
8288
description: "GCC-based D compiler ・ stable, great optimization"
8389
});
8490
items.push(manualSelect = {
85-
label: "Select installation folder",
91+
label: "Select installed executable",
8692
description: "if you have already installed a D compiler that is not being picked up"
8793
});
8894
introQuickPick.items = items;
89-
introQuickPick.show();
95+
introQuickPick.busy = false;
9096

91-
introQuickPick.onDidAccept((e) => {
97+
introQuickPick.onDidAccept(async (e) => {
9298
let selection = <UIQuickPickItem>introQuickPick.selectedItems[0];
9399
if (selection.kind === 2)
94100
return;
@@ -97,23 +103,33 @@ export async function setupCompilersUI() {
97103
if (selection.installInfo) {
98104
showDetectedCompilerInstallPrompt(selection.installInfo);
99105
} else {
106+
function isGlobalInstallSh() {
107+
let dir = getDefaultInstallShDir();
108+
return dir && fs.existsSync(dir);
109+
}
110+
let latest;
100111
switch (selection) {
101112
case dmdItem:
113+
latest = process.platform == "win32" && await readHTTP("http://downloads.dlang.org/releases/LATEST");
102114
showCompilerInstallationPrompt("DMD", [
103115
{ label: "See releases", website: "https://dlang.org/download.html#dmd" },
104-
{ platform: "win32", label: "Run installer", downloadAndRun: "https://s3.us-west-2.amazonaws.com/downloads.dlang.org/releases/2021/dmd-2.098.0.exe" },
116+
latest && { platform: "win32", label: "Run installer", downloadAndRun: "http://downloads.dlang.org/releases/2.x/" + latest + "/dmd-" + latest + ".exe" },
117+
{ label: "Portable install (in existing ~/dlang)", installSh: "install dmd,dub", binTest: "bash", global: true, platform: isGlobalInstallSh },
105118
{ label: "Portable install", installSh: "install dmd,dub", binTest: "bash" },
106119
{ platform: "linux", label: "System install", command: "pacman -S dlang-dmd", binTest: "pacman" },
107120
{ platform: "linux", label: "System install", command: "layman -a dlang", binTest: "layman" },
108121
{ platform: "darwin", label: "System install", command: "brew install dmd", binTest: "brew" },
109122
{ platform: "linux", label: "System install", command: "nix-env -iA nixpkgs.dmd", binTest: "nix-env" },
110123
{ platform: "linux", label: "System install", command: "zypper install dmd", binTest: "zypper" },
124+
{ platform: "linux", label: "System install", command: "xbps-install -S dmd", binTest: "xbps-install" },
111125
]);
112126
break;
113127
case ldcItem:
128+
latest = process.platform == "win32" && await readHTTP("http://ldc-developers.github.io/LATEST");
114129
showCompilerInstallationPrompt("LDC", [
115130
{ label: "See releases", website: "https://github.com/ldc-developers/ldc/releases" },
116-
{ platform: "win32", label: "Run installer", downloadAndRun: "https://github.com/ldc-developers/ldc/releases/download/v1.28.0/ldc2-1.28.0-windows-multilib.exe" },
131+
latest && { platform: "win32", label: "Run installer", downloadAndRun: "https://github.com/ldc-developers/ldc/releases/download/v" + latest + "/ldc2-" + latest + "-windows-multilib.exe" },
132+
{ label: "Portable install (in existing ~/dlang)", installSh: "install ldc,dub", binTest: "bash", global: true, platform: isGlobalInstallSh },
117133
{ label: "Portable install", installSh: "install ldc,dub", binTest: "bash" },
118134
{ label: "System install", command: "brew install ldc", binTest: "brew" },
119135
{ platform: "linux", label: "System install", command: "apk add ldc", binTest: "apk" },
@@ -125,18 +141,22 @@ export async function setupCompilersUI() {
125141
{ platform: "linux", label: "System install", command: "layman -a ldc", binTest: "layman" },
126142
{ platform: "darwin", label: "System install", command: "brew install ldc", binTest: "brew" },
127143
{ platform: "linux", label: "System install", command: "nix-env -i ldc", binTest: "nix-env" },
144+
{ platform: "linux", label: "System install", command: "xbps-install -S ldc", binTest: "xbps-install" },
128145
]);
129146
break;
130147
case gdcItem:
131148
showCompilerInstallationPrompt("GDC", [
132149
{ label: "View Project website", website: "https://gdcproject.org/downloads" },
133150
{ platform: "win32", label: "Install through WinLibs", website: "https://winlibs.com" },
134-
{ platform: "linux", label: "Portable install", installSh: "install gdc,dub" },
151+
// no install.sh for GDC because the version is ancient! (installing gcc 4.8.5, FE 2.068.2)
152+
// { platform: () => isGlobalInstallSh() && process.platform == "linux", label: "Portable install (in existing ~/dlang)", installSh: "install gdc,dub", global: true },
153+
// { platform: "linux", label: "Portable install", installSh: "install gdc,dub" },
135154
{ platform: "linux", label: "System install", command: "pacman -S gcc-d", binTest: "pacman" },
136155
{ platform: "linux", label: "System install", command: "apt install gdc", binTest: "apt" },
137156
]);
138157
break;
139158
case manualSelect:
159+
doManualSelect();
140160
break;
141161
default:
142162
console.error("invalid selection");
@@ -147,20 +167,75 @@ export async function setupCompilersUI() {
147167
});
148168
}
149169

170+
async function readHTTP(uri: string): Promise<string | undefined> {
171+
try {
172+
return (await reqText(undefined, 3000).get(uri)).data;
173+
} catch (e) {
174+
console.log("could not fetch", uri, e);
175+
return undefined;
176+
}
177+
}
178+
179+
async function doManualSelect(): Promise<void> {
180+
let files = await vscode.window.showOpenDialog({
181+
title: "Select compiler executable"
182+
});
183+
if (files && files.length > 0) {
184+
if (files.length > 1) {
185+
vscode.window.showWarningMessage("ignoring more than 1 file");
186+
}
187+
let selectedPath = files[0].fsPath;
188+
let filename = path.basename(selectedPath);
189+
let type = getCompilerTypeFromPrefix(filename);
190+
if (!type) {
191+
let tryAgain = "Try Again";
192+
vscode.window.showErrorMessage("Could not detect compiler type from executable name (tested for DMD, LDC and GDC) - make sure you open the compiler executable and name it correctly!", tryAgain)
193+
.then(b => {
194+
if (b == tryAgain)
195+
doManualSelect();
196+
});
197+
} else {
198+
let result = await checkCompiler(type, selectedPath);
199+
if (!result.has) {
200+
let tryAgain = "Try Again";
201+
vscode.window.showErrorMessage("The selected file was not executable or did not work with. Is the selected file a DMD, LDC or GDB executable?", tryAgain)
202+
.then(b => {
203+
if (b == tryAgain)
204+
doManualSelect();
205+
});
206+
return;
207+
}
208+
209+
if (!result.version && !result.frontendVersion) {
210+
let tryAgain = "Try Again";
211+
let ignore = "Ignore";
212+
let choice = await vscode.window.showWarningMessage("Could not detect the compiler version from the executable. Is the selected file a DMD, LDC or GDB executable?", tryAgain);
213+
if (choice == tryAgain)
214+
return doManualSelect();
215+
else if (choice != ignore)
216+
return;
217+
}
218+
219+
await showDetectedCompilerInstallPrompt(result);
220+
}
221+
}
222+
}
223+
150224
type LabelWebsiteButton = { label: string, platform?: NodeJS.Platform | Function, binTest?: string, website: string };
151225
type LabelDownloadButton = { label: string, platform?: NodeJS.Platform | Function, binTest?: string, downloadAndRun: string };
152226
type LabelCommandButton = { label: string, platform?: NodeJS.Platform | Function, binTest?: string, command: string };
153-
type LabelInstallShButton = { label: string, platform?: NodeJS.Platform | Function, binTest?: string, installSh: string };
227+
type LabelInstallShButton = { label: string, platform?: NodeJS.Platform | Function, binTest?: string, installSh: string, global?: boolean };
154228

155229
type InstallButtonType = LabelWebsiteButton | LabelDownloadButton | LabelCommandButton | LabelInstallShButton;
156230
type InstallQuickPickItem = vscode.QuickPickItem & { button: InstallButtonType };
157231

158-
async function showCompilerInstallationPrompt(name: string, buttons: InstallButtonType[]) {
232+
async function showCompilerInstallationPrompt(name: string, buttons: (InstallButtonType | false | null | undefined | "")[]) {
159233
const installPrompt = vscode.window.createQuickPick();
160234
installPrompt.title = "Install " + name + " compiler";
161235
let items: InstallQuickPickItem[] = [];
162236
for (let i = 0; i < buttons.length; i++) {
163237
const button = buttons[i];
238+
if (!button) continue;
164239
if (button.platform) {
165240
if (typeof button.platform == "function") {
166241
if (!button.platform())
@@ -260,7 +335,8 @@ async function showCompilerInstallationPrompt(name: string, buttons: InstallButt
260335
runTerminal((<LabelCommandButton>selection).command);
261336
} else if ((<LabelInstallShButton>selection).installSh) {
262337
let installSh = codedContext.asAbsolutePath("res/exe/install.sh").replace(/\\/g, '\\\\');
263-
runTerminal((await testBinExists("bash")) + " \"" + installSh + "\" " + (<LabelInstallShButton>selection).installSh);
338+
let installDir = getLocalCompilersDir().replace(/\\/g, '\\\\');
339+
runTerminal(`${await testBinExists("bash")} \"${installSh}\" -p ${installDir} ${(<LabelInstallShButton>selection).installSh}`);
264340
}
265341
}
266342
});
@@ -360,10 +436,16 @@ export async function checkCompilers(): Promise<DetectedCompiler> {
360436
for (let i = 0; i < compilers.length; i++) {
361437
const compiler = compilers[i];
362438
if (compiler.has) {
439+
function isBetterVer(vs: number) {
440+
if (vs == -1) return true;
441+
var a = compilers[i].frontendVersion || compilers[i].version || "0";
442+
var b = compilers[vs].frontendVersion || compilers[vs].version || "0";
443+
return cmpVerGeneric(a, b) > 0;
444+
}
363445
switch (compiler.has) {
364-
case "dmd": dmdIndex = i; break;
365-
case "ldc": ldcIndex = i; break;
366-
case "gdc": gdcIndex = i; break;
446+
case "dmd": if (isBetterVer(dmdIndex)) dmdIndex = i; break;
447+
case "ldc": if (isBetterVer(ldcIndex)) ldcIndex = i; break;
448+
case "gdc": if (isBetterVer(gdcIndex)) gdcIndex = i; break;
367449
default: console.error("unexpected state in code-d?!"); break;
368450
}
369451
}
@@ -379,6 +461,26 @@ export async function checkCompilers(): Promise<DetectedCompiler> {
379461
return { has: false, path: fallbackPath };
380462
}
381463

464+
function cmpVerGeneric(a: string, b: string): number {
465+
var as = a.split(/[\s\.\-]+/g).map(i => parseInt(i)).filter(n => isFinite(n));
466+
var bs = b.split(/[\s\.\-]+/g).map(i => parseInt(i)).filter(n => isFinite(n));
467+
return as < bs ? -1 : as > bs ? 1 : 0;
468+
}
469+
470+
function getDefaultInstallShDir(): string | undefined {
471+
if (process.platform == "win32") {
472+
return process.env.USERPROFILE;
473+
} else if (process.env.HOME) {
474+
return path.join(process.env.HOME, "dlang");
475+
} else {
476+
return undefined;
477+
}
478+
}
479+
480+
function getLocalCompilersDir(): string {
481+
return path.join(determineOutputFolder(), "compilers");
482+
}
483+
382484
let listCompilersCache: DetectedCompiler[] | undefined = undefined;
383485
export async function listCompilers(): Promise<DetectedCompiler[]> {
384486
if (listCompilersCache !== undefined)
@@ -391,6 +493,8 @@ export async function listCompilersImpl(): Promise<DetectedCompiler[]> {
391493
const compilers = ["dmd", "ldc2", "ldc", "gdc", "gcc"];
392494
let ret: DetectedCompiler[] = [];
393495
let fallbackPath: string | undefined = undefined;
496+
497+
// test compilers in $PATH
394498
for (let i = 0; i < compilers.length; i++) {
395499
const check = compilers[i];
396500
let result = await checkCompiler(<any>check);
@@ -402,11 +506,109 @@ export async function listCompilersImpl(): Promise<DetectedCompiler[]> {
402506
i++; // skip ldc / gcc
403507
}
404508
}
509+
510+
async function testInstallShPath(dir: string, type: "dmd" | "ldc" | "gdc") {
511+
let activateFile = process.platform == "win32" ? "activate.bat" : "activate";
512+
let activateContent: string | undefined = await new Promise((resolve) => {
513+
fs.readFile(path.join(dir, activateFile), { encoding: "utf8" }, (err, data) => {
514+
if (err)
515+
return resolve(undefined);
516+
resolve(data)
517+
});
518+
});
519+
520+
if (!activateContent)
521+
return;
522+
523+
let foundPaths: string[] = [];
524+
activatePathEnvironmentRegex.lastIndex = 0;
525+
let m: RegExpMatchArray | null | undefined;
526+
while (m = activatePathEnvironmentRegex.exec(activateContent)) {
527+
// unshift because the scripts are prepending and we want 0 to be most specific
528+
// at least on windows this will prefer the bin64 over bin folder
529+
foundPaths.unshift.apply(foundPaths, m[1].split(process.platform == "win32" ? /;/g : /:/g));
530+
}
531+
532+
for (var i = 0; i < foundPaths.length; i++) {
533+
let exeName: string = type;
534+
if (type == "ldc")
535+
exeName += "2"; // ldc2.exe
536+
if (process.platform == "win32")
537+
exeName += ".exe";
538+
let exePath = path.join(foundPaths[i], exeName);
539+
540+
if (!fs.existsSync(exePath))
541+
continue;
542+
543+
let result = await checkCompiler(type, exePath);
544+
fallbackPath = fallbackPath || result.path;
545+
if (result && result.has) {
546+
result.has = type;
547+
ret.push(result);
548+
break;
549+
}
550+
}
551+
}
552+
553+
// test global install.sh based D compilers
554+
let defaultDir = getDefaultInstallShDir();
555+
if (defaultDir) {
556+
await new Promise((resolve) => {
557+
fs.readdir(defaultDir!, async (err, files) => {
558+
try {
559+
if (err)
560+
return;
561+
for (let i = 0; i < files.length; i++) {
562+
const file = files[i];
563+
const type = getCompilerTypeFromPrefix(file);
564+
if (type)
565+
await testInstallShPath(path.join(defaultDir!, file), type);
566+
}
567+
} finally {
568+
resolve(undefined);
569+
}
570+
});
571+
});
572+
}
573+
574+
// test code-d install.sh based D compilers
575+
await new Promise((resolve) => {
576+
fs.readdir(defaultDir = getLocalCompilersDir(), async (err, files) => {
577+
try {
578+
if (err)
579+
return;
580+
for (let i = 0; i < files.length; i++) {
581+
const file = files[i];
582+
const type = getCompilerTypeFromPrefix(file);
583+
if (type)
584+
await testInstallShPath(path.join(defaultDir!, file), type);
585+
}
586+
} finally {
587+
resolve(undefined);
588+
}
589+
});
590+
});
591+
405592
if (ret.length == 0 && fallbackPath)
406593
ret.push({ has: false, path: fallbackPath });
407594
return ret;
408595
}
409596

597+
// compiler type by checking if the file/foldername starts with ldc/dmd/gdc
598+
function getCompilerTypeFromPrefix(folderName: string): "ldc" | "dmd" | "gdc" | null {
599+
if (folderName.startsWith("dmd"))
600+
return "dmd";
601+
else if (folderName.startsWith("gdc") || folderName.startsWith("gcc"))
602+
return "gdc";
603+
else if (folderName.startsWith("ldc"))
604+
return "ldc";
605+
else
606+
return null;
607+
}
608+
609+
const activatePathEnvironmentRegex = process.platform == "win32"
610+
? /^set\s+PATH="?([^%"]+)"?/gim
611+
: /^(?:export\s+)?PATH="?([^$"]+)"?/gm;
410612
const gdcVersionRegex = /^gcc version\s+v?(\d+(?:\.\d+)+)/gm;
411613
const gdcFeVersionRegex = /^version\s+v?(\d+(?:\.\d+)+)/gm;
412614
const gdcImportPathRegex = /^import path\s*\[\d+\]\s*=\s*(.+)/gm;
@@ -512,13 +714,19 @@ async function checkCompiler(compiler: "dmd" | "ldc" | "ldc2" | "gdc" | "gcc", c
512714

513715
let binExistsCache: { [index: string]: string | false } = {};
514716
async function testBinExists(binary: string): Promise<string | false> {
717+
// common bash install case for windows users
718+
const win32GitBashPath = "C:\\Program Files\\Git\\usr\\bin\\bash.exe";
515719
if (binExistsCache[binary] !== undefined)
516720
return binExistsCache[binary];
517721

518722
try {
519723
let founds = await which(binary, {
520724
all: true
521725
});
726+
if (process.platform == "win32" && (binary.toUpperCase() == "BASH" || binary.toUpperCase() == "BASH.EXE")) {
727+
if (fs.existsSync(win32GitBashPath))
728+
return binExistsCache[binary] = win32GitBashPath;
729+
}
522730
for (let i = 0; i < founds.length; i++) {
523731
const found = founds[i];
524732

0 commit comments

Comments
 (0)