Skip to content

Commit 5178445

Browse files
authored
Merge pull request #254 from bubba/windows-zip-compression
Handle .zip archive binaries
2 parents 1e2ae11 + bab2b10 commit 5178445

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)