Skip to content

Commit bab2b10

Browse files
committed
Handle .zip archive binaries
This is different from just regular zip compression, since the Windows binaries are distributed as zip archives now. So we can't just use zlib, but need to pull in yauzl to handle the archive format
1 parent 1e2ae11 commit bab2b10

File tree

5 files changed

+105
-17
lines changed

5 files changed

+105
-17
lines changed

Changelog.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
### 1.0.0
2+
3+
- vscode-haskell now lives under the Haskell organisation
4+
- Can now download zip archived binaries, which the Windows binaries are now distributed as
5+
- Improve README (@pepeiborra @jaspervdj)
6+
17
### 0.1.1
28

39
- Fix the restart server and import identifier commands

package-lock.json

+36
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@
193193
"@types/lru-cache": "^4.1.2",
194194
"@types/node": "^14.0.3",
195195
"@types/request-promise-native": "^1.0.17",
196+
"@types/yauzl": "^2.9.1",
196197
"husky": "^4.2.5",
197198
"prettier": "^2.0.5",
198199
"pretty-quick": "^2.0.1",
@@ -215,6 +216,7 @@
215216
"lru-cache": "^4.1.5",
216217
"request": "^2.88.2",
217218
"request-promise-native": "^1.0.8",
218-
"vscode-languageclient": "6.1.3"
219+
"vscode-languageclient": "6.1.3",
220+
"yauzl": "^2.10.0"
219221
}
220222
}

src/hlsBinaries.ts

+26-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as child_process from 'child_process';
22
import * as fs from 'fs';
33
import * as https from 'https';
4+
import * as os from 'os';
45
import * as path from 'path';
56
import { env, ExtensionContext, ProgressLocation, Uri, window, WorkspaceFolder } from 'vscode';
67
import { downloadFile, executableExists, userAgentHeader } from './utils';
@@ -18,7 +19,7 @@ interface IAsset {
1819
}
1920

2021
// On Windows the executable needs to be stored somewhere with an .exe extension
21-
const exeExtension = process.platform === 'win32' ? '.exe' : '';
22+
const exeExt = process.platform === 'win32' ? '.exe' : '';
2223

2324
class MissingToolError extends Error {
2425
public readonly tool: string;
@@ -57,6 +58,17 @@ class MissingToolError extends Error {
5758
}
5859
}
5960

61+
// tslint:disable-next-line: max-classes-per-file
62+
class NoBinariesError extends Error {
63+
constructor(hlsVersion: string, ghcVersion?: string) {
64+
if (ghcVersion) {
65+
super(`haskell-language-server ${hlsVersion} for GHC ${ghcVersion} is not available on ${os.type()}`);
66+
} else {
67+
super(`haskell-language-server ${hlsVersion} is not available on ${os.type()}`);
68+
}
69+
}
70+
}
71+
6072
/** Works out what the project's ghc version is, downloading haskell-language-server-wrapper
6173
* if needed. Returns null if there was an error in either downloading the wrapper or
6274
* in working out the ghc version
@@ -97,7 +109,7 @@ async function getProjectGhcVersion(context: ExtensionContext, dir: string, rele
97109

98110
// Otherwise search to see if we previously downloaded the wrapper
99111

100-
const wrapperName = `haskell-language-server-wrapper-${release.tag_name}-${process.platform}${exeExtension}`;
112+
const wrapperName = `haskell-language-server-wrapper-${release.tag_name}-${process.platform}${exeExt}`;
101113
const downloadedWrapper = path.join(context.globalStoragePath, wrapperName);
102114

103115
if (executableExists(downloadedWrapper)) {
@@ -109,14 +121,14 @@ async function getProjectGhcVersion(context: ExtensionContext, dir: string, rele
109121
const githubOS = getGithubOS();
110122
if (githubOS === null) {
111123
// Don't have any binaries available for this platform
112-
throw Error(`Couldn't find any haskell-language-server-wrapper binaries for ${process.platform}`);
124+
throw new NoBinariesError(release.tag_name);
113125
}
114126

115-
const assetName = `haskell-language-server-wrapper-${githubOS}${exeExtension}.gz`;
116-
const wrapperAsset = release.assets.find((x) => x.name === assetName);
127+
const assetName = `haskell-language-server-wrapper-${githubOS}${exeExt}`;
128+
const wrapperAsset = release.assets.find((x) => x.name.startsWith(assetName));
117129

118130
if (!wrapperAsset) {
119-
throw Error(`Couldn't find any ${assetName} binaries for release ${release.tag_name}`);
131+
throw new NoBinariesError(release.tag_name);
120132
}
121133

122134
await downloadFile(
@@ -186,23 +198,25 @@ export async function downloadHaskellLanguageServer(
186198
} else {
187199
await window.showErrorMessage(error.message);
188200
}
201+
} else if (error instanceof NoBinariesError) {
202+
window.showInformationMessage(error.message);
189203
} else {
190204
// We couldn't figure out the right ghc version to download
191205
window.showErrorMessage(`Couldn't figure out what GHC version the project is using:\n${error.message}`);
192206
}
193207
return null;
194208
}
195209

196-
const assetName = `haskell-language-server-${githubOS}-${ghcVersion}${exeExtension}.gz`;
197-
const asset = release?.assets.find((x) => x.name === assetName);
210+
// When searching for binaries, use startsWith because the compression may differ
211+
// between .zip and .gz
212+
const assetName = `haskell-language-server-${githubOS}-${ghcVersion}${exeExt}`;
213+
const asset = release?.assets.find((x) => x.name.startsWith(assetName));
198214
if (!asset) {
199-
window.showErrorMessage(
200-
`Couldn't find any pre-built haskell-language-server binaries for ${githubOS} and ${ghcVersion}`
201-
);
215+
window.showInformationMessage(new NoBinariesError(release.tag_name, ghcVersion).message);
202216
return null;
203217
}
204218

205-
const serverName = `haskell-language-server-${release.tag_name}-${process.platform}-${ghcVersion}${exeExtension}`;
219+
const serverName = `haskell-language-server-${release.tag_name}-${process.platform}-${ghcVersion}${exeExt}`;
206220
const binaryDest = path.join(context.globalStoragePath, serverName);
207221

208222
const title = `Downloading haskell-language-server ${release.tag_name} for GHC ${ghcVersion}`;

src/utils.ts

+34-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as https from 'https';
77
import { extname } from 'path';
88
import * as url from 'url';
99
import { ProgressLocation, window } from 'vscode';
10+
import * as yazul from 'yauzl';
1011
import { createGunzip } from 'zlib';
1112

1213
/** When making http requests to github.com, use this header otherwise
@@ -67,10 +68,39 @@ export async function downloadFile(titleMsg: string, src: string, dest: string):
6768
const fileStream = fs.createWriteStream(downloadDest, { mode: 0o744 });
6869
let curSize = 0;
6970

70-
// Decompress it if it's a gzip
71-
const needsUnzip = res.headers['content-type'] === 'application/gzip' || extname(srcUrl.path ?? '') === '.gz';
72-
if (needsUnzip) {
73-
res.pipe(createGunzip()).pipe(fileStream);
71+
// Decompress it if it's a gzip or zip
72+
const needsGunzip =
73+
res.headers['content-type'] === 'application/gzip' || extname(srcUrl.path ?? '') === '.gz';
74+
const needsUnzip = res.headers['content-type'] === 'application/zip' || extname(srcUrl.path ?? '') === '.zip';
75+
if (needsGunzip) {
76+
const gunzip = createGunzip();
77+
gunzip.on('error', reject);
78+
res.pipe(gunzip).pipe(fileStream);
79+
} else if (needsUnzip) {
80+
const zipDest = downloadDest + '.zip';
81+
const zipFs = fs.createWriteStream(zipDest);
82+
zipFs.on('error', reject);
83+
zipFs.on('close', () => {
84+
yazul.open(zipDest, (err, zipfile) => {
85+
if (err) {
86+
throw err;
87+
}
88+
if (!zipfile) {
89+
throw Error("Couldn't decompress zip");
90+
}
91+
92+
// We only expect *one* file inside each zip
93+
zipfile.on('entry', (entry: yazul.Entry) => {
94+
zipfile.openReadStream(entry, (err2, readStream) => {
95+
if (err2) {
96+
throw err2;
97+
}
98+
readStream?.pipe(fileStream);
99+
});
100+
});
101+
});
102+
});
103+
res.pipe(zipFs);
74104
} else {
75105
res.pipe(fileStream);
76106
}

0 commit comments

Comments
 (0)