mirror of
https://github.com/Microsoft/vscode
synced 2024-10-02 09:18:59 +00:00
cli: cleanup build (#190213)
- Remove the `prepare` script entirely - Variables are now populated from the product.json during build. Most variables are mapped automatically, with some special handling in a few cases. `build.rs` is now much more self-contained. - Look for the `product.overrides.json` for vscode developers instead of looking for a peer `vscode-distro` folder Fixes #178691
This commit is contained in:
parent
2e9459b34c
commit
52840e3ca5
|
@ -14,8 +14,6 @@ steps:
|
|||
versionSpec: "18.x"
|
||||
|
||||
- ${{ if ne(parameters.VSCODE_QUALITY, 'oss') }}:
|
||||
- template: ../distro/download-distro.yml
|
||||
|
||||
# Install yarn as the ARM64 build agent is using vanilla Ubuntu
|
||||
- ${{ if eq(parameters.VSCODE_BUILD_ALPINE_ARM64, true) }}:
|
||||
- task: Npm@1
|
||||
|
@ -30,8 +28,7 @@ steps:
|
|||
workingDirectory: build
|
||||
displayName: Install pipeline build
|
||||
|
||||
- script: node .build/distro/cli-patches/index.js
|
||||
displayName: Apply distro patches
|
||||
- template: ../cli/cli-apply-patches.yml
|
||||
|
||||
- task: Npm@1
|
||||
displayName: Download openssl prebuilt
|
||||
|
@ -56,13 +53,6 @@ steps:
|
|||
sudo ln -s "/usr/bin/g++" "/usr/bin/musl-g++" || echo "link exists"
|
||||
displayName: Install musl build dependencies
|
||||
|
||||
- script: node build/azure-pipelines/cli/prepare.js
|
||||
displayName: Prepare CLI build
|
||||
env:
|
||||
VSCODE_CLI_PREPARE_ROOT: $(Build.SourcesDirectory)/.build/distro
|
||||
VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }}
|
||||
GITHUB_TOKEN: "$(github-distro-mixin-password)"
|
||||
|
||||
- template: ../cli/install-rust-posix.yml
|
||||
parameters:
|
||||
targets:
|
||||
|
@ -76,6 +66,7 @@ steps:
|
|||
parameters:
|
||||
VSCODE_CLI_TARGET: aarch64-unknown-linux-musl
|
||||
VSCODE_CLI_ARTIFACT: vscode_cli_alpine_arm64_cli
|
||||
VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }}
|
||||
VSCODE_CLI_ENV:
|
||||
CXX_aarch64-unknown-linux-musl: musl-g++
|
||||
CC_aarch64-unknown-linux-musl: musl-gcc
|
||||
|
@ -88,6 +79,7 @@ steps:
|
|||
parameters:
|
||||
VSCODE_CLI_TARGET: x86_64-unknown-linux-musl
|
||||
VSCODE_CLI_ARTIFACT: vscode_cli_alpine_x64_cli
|
||||
VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }}
|
||||
VSCODE_CLI_ENV:
|
||||
CXX_aarch64-unknown-linux-musl: musl-g++
|
||||
CC_aarch64-unknown-linux-musl: musl-gcc
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
steps:
|
||||
- template: ../distro/download-distro.yml
|
||||
|
||||
- script: node build/azure-pipelines/distro/mixin-quality
|
||||
displayName: Mixin distro quality
|
||||
|
||||
- script: node .build/distro/cli-patches/index.js
|
||||
displayName: Apply distro patches
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
parameters:
|
||||
- name: VSCODE_QUALITY
|
||||
type: string
|
||||
- name: VSCODE_CLI_TARGET
|
||||
type: string
|
||||
- name: VSCODE_CLI_ARTIFACT
|
||||
|
@ -11,6 +13,13 @@ parameters:
|
|||
default: false
|
||||
|
||||
steps:
|
||||
- ${{ if eq(parameters.VSCODE_QUALITY, 'oss') }}:
|
||||
- pwsh: Write-Host "##vso[task.setvariable variable=VSCODE_CLI_PRODUCT_JSON]$(Build.SourcesDirectory)/product.json"
|
||||
displayName: Set product.json path
|
||||
- ${{ else }}:
|
||||
- pwsh: Write-Host "##vso[task.setvariable variable=VSCODE_CLI_PRODUCT_JSON]$(Build.SourcesDirectory)/.build/distro/mixin/${{ parameters.VSCODE_QUALITY }}/product.json"
|
||||
displayName: Set product.json path
|
||||
|
||||
- ${{ if parameters.VSCODE_CHECK_ONLY }}:
|
||||
- script: rustup component add clippy && cargo clippy --target ${{ parameters.VSCODE_CLI_TARGET }} --bin=code
|
||||
displayName: Lint ${{ parameters.VSCODE_CLI_TARGET }}
|
||||
|
@ -26,6 +35,7 @@ steps:
|
|||
workingDirectory: $(Build.SourcesDirectory)/cli
|
||||
env:
|
||||
CARGO_NET_GIT_FETCH_WITH_CLI: true
|
||||
VSCODE_CLI_COMMIT: $(Build.SourceVersion)
|
||||
${{ each pair in parameters.VSCODE_CLI_ENV }}:
|
||||
${{ pair.key }}: ${{ pair.value }}
|
||||
|
||||
|
@ -33,6 +43,11 @@ steps:
|
|||
- powershell: |
|
||||
. build/azure-pipelines/win32/exec.ps1
|
||||
$ErrorActionPreference = "Stop"
|
||||
$AppProductJson = Get-Content -Raw -Path "$env:VSCODE_CLI_PRODUCT_JSON" | ConvertFrom-Json
|
||||
$env:VSCODE_CLI_APPLICATION_NAME = $AppProductJson.applicationName
|
||||
|
||||
Write-Host "##vso[task.setvariable variable=VSCODE_CLI_APPLICATION_NAME]$env:VSCODE_CLI_APPLICATION_NAME"
|
||||
|
||||
Move-Item -Path $(Build.SourcesDirectory)/cli/target/${{ parameters.VSCODE_CLI_TARGET }}/release/code.exe -Destination "$(Build.ArtifactStagingDirectory)/${env:VSCODE_CLI_APPLICATION_NAME}.exe"
|
||||
|
||||
- task: ArchiveFiles@2
|
||||
|
@ -49,7 +64,10 @@ steps:
|
|||
- ${{ else }}:
|
||||
- script: |
|
||||
set -e
|
||||
mv $(Build.SourcesDirectory)/cli/target/${{ parameters.VSCODE_CLI_TARGET }}/release/code $(Build.ArtifactStagingDirectory)/$(VSCODE_CLI_APPLICATION_NAME)
|
||||
VSCODE_CLI_APPLICATION_NAME=$(node -p "require(\"$VSCODE_CLI_PRODUCT_JSON\").applicationName")
|
||||
echo "##vso[task.setvariable variable=VSCODE_CLI_APPLICATION_NAME]$VSCODE_CLI_APPLICATION_NAME"
|
||||
|
||||
mv $(Build.SourcesDirectory)/cli/target/${{ parameters.VSCODE_CLI_TARGET }}/release/code $(Build.ArtifactStagingDirectory)/$VSCODE_CLI_APPLICATION_NAME
|
||||
|
||||
- ${{ if contains(parameters.VSCODE_CLI_TARGET, '-darwin') }}:
|
||||
- task: ArchiveFiles@2
|
||||
|
|
|
@ -1,93 +0,0 @@
|
|||
"use strict";
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const getVersion_1 = require("../../lib/getVersion");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const packageJson = require("../../../package.json");
|
||||
const root = process.env.VSCODE_CLI_PREPARE_ROOT || path.dirname(path.dirname(path.dirname(__dirname)));
|
||||
const readJSON = (path) => JSON.parse(fs.readFileSync(path, 'utf8'));
|
||||
let productJsonPath;
|
||||
const isOSS = process.env.VSCODE_QUALITY === 'oss' || !process.env.VSCODE_QUALITY;
|
||||
if (isOSS) {
|
||||
productJsonPath = path.join(root, 'product.json');
|
||||
}
|
||||
else {
|
||||
productJsonPath = path.join(root, 'mixin', process.env.VSCODE_QUALITY, 'product.json');
|
||||
}
|
||||
console.error('Loading product.json from', productJsonPath);
|
||||
const product = readJSON(productJsonPath);
|
||||
const allProductsAndQualities = isOSS ? [product] : fs.readdirSync(path.join(root, 'mixin'))
|
||||
.map(quality => ({ quality, json: readJSON(path.join(root, 'mixin', quality, 'product.json')) }));
|
||||
const commit = (0, getVersion_1.getVersion)(root);
|
||||
const makeQualityMap = (m) => {
|
||||
const output = {};
|
||||
for (const { quality, json } of allProductsAndQualities) {
|
||||
output[quality] = m(json, quality);
|
||||
}
|
||||
return output;
|
||||
};
|
||||
/**
|
||||
* Sets build environment variables for the CLI for current contextual info.
|
||||
*/
|
||||
const setLauncherEnvironmentVars = () => {
|
||||
const vars = new Map([
|
||||
['VSCODE_CLI_ALREADY_PREPARED', 'true'],
|
||||
['VSCODE_CLI_REMOTE_LICENSE_TEXT', product.serverLicense?.join('\\n')],
|
||||
['VSCODE_CLI_REMOTE_LICENSE_PROMPT', product.serverLicensePrompt],
|
||||
['VSCODE_CLI_AI_KEY', product.aiConfig?.cliKey],
|
||||
['VSCODE_CLI_AI_ENDPOINT', product.aiConfig?.cliEndpoint],
|
||||
['VSCODE_CLI_VERSION', packageJson.version],
|
||||
['VSCODE_CLI_UPDATE_ENDPOINT', product.updateUrl],
|
||||
['VSCODE_CLI_QUALITY', product.quality],
|
||||
['VSCODE_CLI_NAME_SHORT', product.nameShort],
|
||||
['VSCODE_CLI_NAME_LONG', product.nameLong],
|
||||
['VSCODE_CLI_QUALITYLESS_PRODUCT_NAME', product.nameLong.replace(/ - [a-z]+$/i, '')],
|
||||
['VSCODE_CLI_DOCUMENTATION_URL', product.documentationUrl],
|
||||
['VSCODE_CLI_APPLICATION_NAME', product.applicationName],
|
||||
['VSCODE_CLI_EDITOR_WEB_URL', product.tunnelApplicationConfig?.editorWebUrl],
|
||||
['VSCODE_CLI_TUNNEL_SERVICE_MUTEX', product.win32TunnelServiceMutex],
|
||||
['VSCODE_CLI_TUNNEL_CLI_MUTEX', product.win32TunnelMutex],
|
||||
['VSCODE_CLI_COMMIT', commit],
|
||||
['VSCODE_CLI_DEFAULT_PARENT_DATA_DIR', product.dataFolderName],
|
||||
[
|
||||
'VSCODE_CLI_WIN32_APP_IDS',
|
||||
!isOSS && JSON.stringify(makeQualityMap(json => Object.entries(json)
|
||||
.filter(([key]) => /^win32.*AppId$/.test(key))
|
||||
.map(([, value]) => String(value).replace(/[{}]/g, '')))),
|
||||
],
|
||||
[
|
||||
'VSCODE_CLI_NAME_LONG_MAP',
|
||||
!isOSS && JSON.stringify(makeQualityMap(json => json.nameLong)),
|
||||
],
|
||||
[
|
||||
'VSCODE_CLI_APPLICATION_NAME_MAP',
|
||||
!isOSS && JSON.stringify(makeQualityMap(json => json.applicationName)),
|
||||
],
|
||||
[
|
||||
'VSCODE_CLI_SERVER_NAME_MAP',
|
||||
!isOSS && JSON.stringify(makeQualityMap(json => json.serverApplicationName)),
|
||||
],
|
||||
[
|
||||
'VSCODE_CLI_QUALITY_DOWNLOAD_URIS',
|
||||
!isOSS && JSON.stringify(makeQualityMap(json => json.downloadUrl)),
|
||||
],
|
||||
]);
|
||||
if (process.env.VSCODE_CLI_PREPARE_OUTPUT === 'json') {
|
||||
console.log(JSON.stringify([...vars].filter(([, v]) => !!v)));
|
||||
}
|
||||
else {
|
||||
for (const [key, value] of vars) {
|
||||
if (value) {
|
||||
console.log(`##vso[task.setvariable variable=${key}]${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if (require.main === module) {
|
||||
setLauncherEnvironmentVars();
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJlcGFyZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInByZXBhcmUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBOzs7Z0dBR2dHOztBQUVoRyxxREFBa0Q7QUFDbEQseUJBQXlCO0FBQ3pCLDZCQUE2QjtBQUM3QixxREFBcUQ7QUFFckQsTUFBTSxJQUFJLEdBQUcsT0FBTyxDQUFDLEdBQUcsQ0FBQyx1QkFBdUIsSUFBSSxJQUFJLENBQUMsT0FBTyxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFDeEcsTUFBTSxRQUFRLEdBQUcsQ0FBQyxJQUFZLEVBQUUsRUFBRSxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDLFlBQVksQ0FBQyxJQUFJLEVBQUUsTUFBTSxDQUFDLENBQUMsQ0FBQztBQUU3RSxJQUFJLGVBQXVCLENBQUM7QUFDNUIsTUFBTSxLQUFLLEdBQUcsT0FBTyxDQUFDLEdBQUcsQ0FBQyxjQUFjLEtBQUssS0FBSyxJQUFJLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxjQUFjLENBQUM7QUFDbEYsSUFBSSxLQUFLLEVBQUU7SUFDVixlQUFlLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxJQUFJLEVBQUUsY0FBYyxDQUFDLENBQUM7Q0FDbEQ7S0FBTTtJQUNOLGVBQWUsR0FBRyxJQUFJLENBQUMsSUFBSSxDQUFDLElBQUksRUFBRSxPQUFPLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxjQUFlLEVBQUUsY0FBYyxDQUFDLENBQUM7Q0FDeEY7QUFFRCxPQUFPLENBQUMsS0FBSyxDQUFDLDJCQUEyQixFQUFFLGVBQWUsQ0FBQyxDQUFDO0FBQzVELE1BQU0sT0FBTyxHQUFHLFFBQVEsQ0FBQyxlQUFlLENBQUMsQ0FBQztBQUMxQyxNQUFNLHVCQUF1QixHQUFHLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFD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
|
|
@ -1,99 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { getVersion } from '../../lib/getVersion';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as packageJson from '../../../package.json';
|
||||
|
||||
const root = process.env.VSCODE_CLI_PREPARE_ROOT || path.dirname(path.dirname(path.dirname(__dirname)));
|
||||
const readJSON = (path: string) => JSON.parse(fs.readFileSync(path, 'utf8'));
|
||||
|
||||
let productJsonPath: string;
|
||||
const isOSS = process.env.VSCODE_QUALITY === 'oss' || !process.env.VSCODE_QUALITY;
|
||||
if (isOSS) {
|
||||
productJsonPath = path.join(root, 'product.json');
|
||||
} else {
|
||||
productJsonPath = path.join(root, 'mixin', process.env.VSCODE_QUALITY!, 'product.json');
|
||||
}
|
||||
|
||||
console.error('Loading product.json from', productJsonPath);
|
||||
const product = readJSON(productJsonPath);
|
||||
const allProductsAndQualities = isOSS ? [product] : fs.readdirSync(path.join(root, 'mixin'))
|
||||
.map(quality => ({ quality, json: readJSON(path.join(root, 'mixin', quality, 'product.json')) }));
|
||||
const commit = getVersion(root);
|
||||
|
||||
const makeQualityMap = <T>(m: (productJson: any, quality: string) => T): Record<string, T> => {
|
||||
const output: Record<string, T> = {};
|
||||
for (const { quality, json } of allProductsAndQualities) {
|
||||
output[quality] = m(json, quality);
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets build environment variables for the CLI for current contextual info.
|
||||
*/
|
||||
const setLauncherEnvironmentVars = () => {
|
||||
const vars = new Map([
|
||||
['VSCODE_CLI_ALREADY_PREPARED', 'true'],
|
||||
['VSCODE_CLI_REMOTE_LICENSE_TEXT', product.serverLicense?.join('\\n')],
|
||||
['VSCODE_CLI_REMOTE_LICENSE_PROMPT', product.serverLicensePrompt],
|
||||
['VSCODE_CLI_AI_KEY', product.aiConfig?.cliKey],
|
||||
['VSCODE_CLI_AI_ENDPOINT', product.aiConfig?.cliEndpoint],
|
||||
['VSCODE_CLI_VERSION', packageJson.version],
|
||||
['VSCODE_CLI_UPDATE_ENDPOINT', product.updateUrl],
|
||||
['VSCODE_CLI_QUALITY', product.quality],
|
||||
['VSCODE_CLI_NAME_SHORT', product.nameShort],
|
||||
['VSCODE_CLI_NAME_LONG', product.nameLong],
|
||||
['VSCODE_CLI_QUALITYLESS_PRODUCT_NAME', product.nameLong.replace(/ - [a-z]+$/i, '')],
|
||||
['VSCODE_CLI_DOCUMENTATION_URL', product.documentationUrl],
|
||||
['VSCODE_CLI_APPLICATION_NAME', product.applicationName],
|
||||
['VSCODE_CLI_EDITOR_WEB_URL', product.tunnelApplicationConfig?.editorWebUrl],
|
||||
['VSCODE_CLI_TUNNEL_SERVICE_MUTEX', product.win32TunnelServiceMutex],
|
||||
['VSCODE_CLI_TUNNEL_CLI_MUTEX', product.win32TunnelMutex],
|
||||
['VSCODE_CLI_COMMIT', commit],
|
||||
['VSCODE_CLI_DEFAULT_PARENT_DATA_DIR', product.dataFolderName],
|
||||
[
|
||||
'VSCODE_CLI_WIN32_APP_IDS',
|
||||
!isOSS && JSON.stringify(
|
||||
makeQualityMap(json => Object.entries(json)
|
||||
.filter(([key]) => /^win32.*AppId$/.test(key))
|
||||
.map(([, value]) => String(value).replace(/[{}]/g, ''))),
|
||||
),
|
||||
],
|
||||
[
|
||||
'VSCODE_CLI_NAME_LONG_MAP',
|
||||
!isOSS && JSON.stringify(makeQualityMap(json => json.nameLong)),
|
||||
],
|
||||
[
|
||||
'VSCODE_CLI_APPLICATION_NAME_MAP',
|
||||
!isOSS && JSON.stringify(makeQualityMap(json => json.applicationName)),
|
||||
],
|
||||
[
|
||||
'VSCODE_CLI_SERVER_NAME_MAP',
|
||||
!isOSS && JSON.stringify(makeQualityMap(json => json.serverApplicationName)),
|
||||
],
|
||||
[
|
||||
'VSCODE_CLI_QUALITY_DOWNLOAD_URIS',
|
||||
!isOSS && JSON.stringify(makeQualityMap(json => json.downloadUrl)),
|
||||
],
|
||||
]);
|
||||
|
||||
if (process.env.VSCODE_CLI_PREPARE_OUTPUT === 'json') {
|
||||
console.log(JSON.stringify([...vars].filter(([, v]) => !!v)));
|
||||
} else {
|
||||
for (const [key, value] of vars) {
|
||||
if (value) {
|
||||
console.log(`##vso[task.setvariable variable=${key}]${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
setLauncherEnvironmentVars();
|
||||
}
|
|
@ -34,13 +34,6 @@ steps:
|
|||
tar -xvzf $(Build.ArtifactStagingDirectory)/vscode-internal-openssl-prebuilt-0.0.8.tgz --strip-components=1 --directory=$(Build.ArtifactStagingDirectory)/openssl
|
||||
displayName: Extract openssl prebuilt
|
||||
|
||||
- script: node build/azure-pipelines/cli/prepare.js
|
||||
displayName: Prepare CLI build
|
||||
env:
|
||||
VSCODE_CLI_PREPARE_ROOT: $(Build.SourcesDirectory)/.build/distro
|
||||
VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }}
|
||||
GITHUB_TOKEN: "$(github-distro-mixin-password)"
|
||||
|
||||
- template: ../cli/install-rust-posix.yml
|
||||
parameters:
|
||||
targets:
|
||||
|
@ -52,6 +45,7 @@ steps:
|
|||
- ${{ if eq(parameters.VSCODE_BUILD_MACOS, true) }}:
|
||||
- template: ../cli/cli-compile-and-publish.yml
|
||||
parameters:
|
||||
VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }}
|
||||
VSCODE_CLI_TARGET: x86_64-apple-darwin
|
||||
VSCODE_CLI_ARTIFACT: unsigned_vscode_cli_darwin_x64_cli
|
||||
VSCODE_CHECK_ONLY: ${{ parameters.VSCODE_CHECK_ONLY }}
|
||||
|
@ -62,6 +56,7 @@ steps:
|
|||
- ${{ if eq(parameters.VSCODE_BUILD_MACOS_ARM64, true) }}:
|
||||
- template: ../cli/cli-compile-and-publish.yml
|
||||
parameters:
|
||||
VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }}
|
||||
VSCODE_CLI_TARGET: aarch64-apple-darwin
|
||||
VSCODE_CLI_ARTIFACT: unsigned_vscode_cli_darwin_arm64_cli
|
||||
VSCODE_CHECK_ONLY: ${{ parameters.VSCODE_CHECK_ONLY }}
|
||||
|
|
|
@ -45,13 +45,6 @@ steps:
|
|||
- bash: sudo apt-get install -yq gcc-aarch64-linux-gnu g++-aarch64-linux-gnu binutils-aarch64-linux-gnu
|
||||
displayName: Install arm64 toolchains
|
||||
|
||||
- script: node build/azure-pipelines/cli/prepare.js
|
||||
displayName: Prepare CLI build
|
||||
env:
|
||||
VSCODE_CLI_PREPARE_ROOT: $(Build.SourcesDirectory)/.build/distro
|
||||
VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }}
|
||||
GITHUB_TOKEN: "$(github-distro-mixin-password)"
|
||||
|
||||
- template: ../cli/install-rust-posix.yml
|
||||
parameters:
|
||||
targets:
|
||||
|
@ -65,6 +58,7 @@ steps:
|
|||
- ${{ if eq(parameters.VSCODE_BUILD_LINUX_ARM64, true) }}:
|
||||
- template: ../cli/cli-compile-and-publish.yml
|
||||
parameters:
|
||||
VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }}
|
||||
VSCODE_CLI_TARGET: aarch64-unknown-linux-gnu
|
||||
VSCODE_CLI_ARTIFACT: vscode_cli_linux_arm64_cli
|
||||
VSCODE_CHECK_ONLY: ${{ parameters.VSCODE_CHECK_ONLY }}
|
||||
|
@ -76,6 +70,7 @@ steps:
|
|||
- ${{ if eq(parameters.VSCODE_BUILD_LINUX, true) }}:
|
||||
- template: ../cli/cli-compile-and-publish.yml
|
||||
parameters:
|
||||
VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }}
|
||||
VSCODE_CLI_TARGET: x86_64-unknown-linux-gnu
|
||||
VSCODE_CLI_ARTIFACT: vscode_cli_linux_x64_cli
|
||||
VSCODE_CHECK_ONLY: ${{ parameters.VSCODE_CHECK_ONLY }}
|
||||
|
@ -86,6 +81,7 @@ steps:
|
|||
- ${{ if eq(parameters.VSCODE_BUILD_LINUX_ARMHF, true) }}:
|
||||
- template: ../cli/cli-compile-and-publish.yml
|
||||
parameters:
|
||||
VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }}
|
||||
VSCODE_CLI_TARGET: armv7-unknown-linux-gnueabihf
|
||||
VSCODE_CLI_ARTIFACT: vscode_cli_linux_armhf_cli
|
||||
VSCODE_CHECK_ONLY: ${{ parameters.VSCODE_CHECK_ONLY }}
|
||||
|
|
|
@ -36,13 +36,6 @@ steps:
|
|||
tar -xvzf $(Build.ArtifactStagingDirectory)/vscode-internal-openssl-prebuilt-0.0.8.tgz --strip-components=1 --directory=$(Build.ArtifactStagingDirectory)/openssl
|
||||
displayName: Extract openssl prebuilt
|
||||
|
||||
- powershell: node build/azure-pipelines/cli/prepare.js
|
||||
displayName: Prepare CLI build
|
||||
env:
|
||||
VSCODE_CLI_PREPARE_ROOT: $(Build.SourcesDirectory)/.build/distro
|
||||
VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }}
|
||||
GITHUB_TOKEN: "$(github-distro-mixin-password)"
|
||||
|
||||
- template: ../cli/install-rust-win32.yml
|
||||
parameters:
|
||||
targets:
|
||||
|
@ -56,6 +49,7 @@ steps:
|
|||
- ${{ if eq(parameters.VSCODE_BUILD_WIN32, true) }}:
|
||||
- template: ../cli/cli-compile-and-publish.yml
|
||||
parameters:
|
||||
VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }}
|
||||
VSCODE_CLI_TARGET: x86_64-pc-windows-msvc
|
||||
VSCODE_CLI_ARTIFACT: unsigned_vscode_cli_win32_x64_cli
|
||||
VSCODE_CHECK_ONLY: ${{ parameters.VSCODE_CHECK_ONLY }}
|
||||
|
@ -67,6 +61,7 @@ steps:
|
|||
- ${{ if eq(parameters.VSCODE_BUILD_WIN32_ARM64, true) }}:
|
||||
- template: ../cli/cli-compile-and-publish.yml
|
||||
parameters:
|
||||
VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }}
|
||||
VSCODE_CLI_TARGET: aarch64-pc-windows-msvc
|
||||
VSCODE_CLI_ARTIFACT: unsigned_vscode_cli_win32_arm64_cli
|
||||
VSCODE_CHECK_ONLY: ${{ parameters.VSCODE_CHECK_ONLY }}
|
||||
|
@ -78,6 +73,7 @@ steps:
|
|||
- ${{ if eq(parameters.VSCODE_BUILD_WIN32_32BIT, true) }}:
|
||||
- template: ../cli/cli-compile-and-publish.yml
|
||||
parameters:
|
||||
VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }}
|
||||
VSCODE_CLI_TARGET: i686-pc-windows-msvc
|
||||
VSCODE_CLI_ARTIFACT: unsigned_vscode_cli_win32_ia32_cli
|
||||
VSCODE_CHECK_ONLY: ${{ parameters.VSCODE_CHECK_ONLY }}
|
||||
|
|
|
@ -56,7 +56,7 @@ bytes = "1.4.0"
|
|||
tar = "0.4.38"
|
||||
|
||||
[build-dependencies]
|
||||
serde = "1.0.163"
|
||||
serde = { version="1.0.163", features = ["derive"] }
|
||||
serde_json = "1.0.96"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
|
|
151
cli/build.rs
151
cli/build.rs
|
@ -6,53 +6,146 @@
|
|||
const FILE_HEADER: &str = "/*---------------------------------------------------------------------------------------------\n * Copyright (c) Microsoft Corporation. All rights reserved.\n * Licensed under the MIT License. See License.txt in the project root for license information.\n *--------------------------------------------------------------------------------------------*/";
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env, fs, io,
|
||||
path::PathBuf,
|
||||
process::{self, Command},
|
||||
path::{Path, PathBuf},
|
||||
process::{self},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use serde::{de::DeserializeOwned, Deserialize};
|
||||
use serde_json::Value;
|
||||
|
||||
fn main() {
|
||||
let files = enumerate_source_files().expect("expected to enumerate files");
|
||||
ensure_file_headers(&files).expect("expected to ensure file headers");
|
||||
apply_build_environment_variables();
|
||||
}
|
||||
|
||||
fn apply_build_environment_variables() {
|
||||
// only do this for local, debug builds
|
||||
if env::var("PROFILE").unwrap() != "debug" || env::var("VSCODE_CLI_ALREADY_PREPARED").is_ok() {
|
||||
return;
|
||||
fn camel_case_to_constant_case(key: &str) -> String {
|
||||
let mut output = String::new();
|
||||
let mut prev_upper = false;
|
||||
for c in key.chars() {
|
||||
if c.is_uppercase() {
|
||||
if prev_upper {
|
||||
output.push(c.to_ascii_lowercase());
|
||||
} else {
|
||||
output.push('_');
|
||||
output.push(c.to_ascii_uppercase());
|
||||
}
|
||||
prev_upper = true;
|
||||
} else if c.is_lowercase() {
|
||||
output.push(c.to_ascii_uppercase());
|
||||
prev_upper = false;
|
||||
} else {
|
||||
output.push(c);
|
||||
prev_upper = false;
|
||||
}
|
||||
}
|
||||
|
||||
let pkg_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
let mut cmd = Command::new(env::var("NODE_PATH").unwrap_or_else(|_| "node".to_string()));
|
||||
cmd.arg("../build/azure-pipelines/cli/prepare.js");
|
||||
cmd.current_dir(&pkg_dir);
|
||||
cmd.env("VSCODE_CLI_PREPARE_OUTPUT", "json");
|
||||
output
|
||||
}
|
||||
|
||||
let mut distro_location = PathBuf::from_str(&pkg_dir).unwrap();
|
||||
distro_location.pop(); // vscode dir
|
||||
distro_location.pop(); // parent dir
|
||||
distro_location.push("vscode-distro"); // distro dir, perhaps?
|
||||
if distro_location.exists() {
|
||||
cmd.env("VSCODE_CLI_PREPARE_ROOT", distro_location);
|
||||
cmd.env("VSCODE_QUALITY", "insider");
|
||||
fn set_env_vars_from_map_keys(prefix: &str, map: impl IntoIterator<Item = (String, Value)>) {
|
||||
let mut win32_app_ids = vec![];
|
||||
|
||||
for (key, value) in map {
|
||||
//#region special handling
|
||||
let value = match key.as_str() {
|
||||
"tunnelServerQualities" | "serverLicense" => {
|
||||
Value::String(serde_json::to_string(&value).unwrap())
|
||||
}
|
||||
"nameLong" => {
|
||||
if let Value::String(s) = &value {
|
||||
let idx = s.find(" - ");
|
||||
println!(
|
||||
"cargo:rustc-env=VSCODE_CLI_QUALITYLESS_PRODUCT_NAME={}",
|
||||
idx.map(|i| &s[..i]).unwrap_or(s)
|
||||
);
|
||||
}
|
||||
|
||||
value
|
||||
}
|
||||
"tunnelApplicationConfig" => {
|
||||
if let Value::Object(v) = value {
|
||||
set_env_vars_from_map_keys(&format!("{}_{}", prefix, "TUNNEL"), v);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
_ => value,
|
||||
};
|
||||
if key.contains("win32") && key.contains("AppId") {
|
||||
if let Value::String(s) = value {
|
||||
win32_app_ids.push(s);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
if let Value::String(s) = value {
|
||||
println!(
|
||||
"cargo:rustc-env={}_{}={}",
|
||||
prefix,
|
||||
camel_case_to_constant_case(&key),
|
||||
s
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let output = cmd.output().expect("expected to run prepare script");
|
||||
if !output.status.success() {
|
||||
eprint!(
|
||||
"error running prepare script: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
if !win32_app_ids.is_empty() {
|
||||
println!(
|
||||
"cargo:rustc-env=VSCODE_CLI_WIN32_APP_IDS={}",
|
||||
win32_app_ids.join(",")
|
||||
);
|
||||
process::exit(output.status.code().unwrap_or(1));
|
||||
}
|
||||
}
|
||||
|
||||
let vars = serde_json::from_slice::<Vec<(String, String)>>(&output.stdout)
|
||||
.expect("expected to deserialize output");
|
||||
for (key, value) in vars {
|
||||
println!("cargo:rustc-env={}={}", key, value);
|
||||
}
|
||||
fn read_json_from_path<T>(path: &Path) -> T
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let mut file = fs::File::open(path).expect("failed to open file");
|
||||
serde_json::from_reader(&mut file).expect("failed to deserialize JSON")
|
||||
}
|
||||
|
||||
fn apply_build_from_product_json(path: &Path) {
|
||||
let json: HashMap<String, Value> = read_json_from_path(path);
|
||||
set_env_vars_from_map_keys("VSCODE_CLI", json);
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PackageJson {
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
fn apply_build_environment_variables() {
|
||||
let repo_dir = env::current_dir().unwrap().join("..");
|
||||
let package_json = read_json_from_path::<PackageJson>(&repo_dir.join("package.json"));
|
||||
println!(
|
||||
"cargo:rustc-env=VSCODE_CLI_VERSION={}",
|
||||
package_json.version
|
||||
);
|
||||
|
||||
match env::var("VSCODE_CLI_PRODUCT_JSON") {
|
||||
Ok(v) => {
|
||||
let path = if cfg!(windows) {
|
||||
PathBuf::from_str(&v.replace('/', "\\")).unwrap()
|
||||
} else {
|
||||
PathBuf::from_str(&v).unwrap()
|
||||
};
|
||||
println!("cargo:warning=loading product.json from <{:?}>", path);
|
||||
apply_build_from_product_json(&path);
|
||||
}
|
||||
|
||||
Err(_) => {
|
||||
apply_build_from_product_json(&repo_dir.join("product.json"));
|
||||
|
||||
let overrides = repo_dir.join("product.overrides.json");
|
||||
if overrides.exists() {
|
||||
apply_build_from_product_json(&overrides);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn ensure_file_headers(files: &[PathBuf]) -> Result<(), io::Error> {
|
||||
|
|
|
@ -304,7 +304,7 @@ pub enum VersionSubcommand {
|
|||
#[derive(Args, Debug, Clone)]
|
||||
pub struct UseVersionArgs {
|
||||
/// The version of the editor you want to use. Can be "stable", "insiders",
|
||||
/// a version number, or an absolute path to an existing install.
|
||||
/// or an absolute path to an existing install.
|
||||
#[clap(value_name = "stable | insiders | x.y.z | path")]
|
||||
pub name: String,
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
use serde::Deserialize;
|
||||
use std::{collections::HashMap, io::IsTerminal};
|
||||
|
||||
use const_format::concatcp;
|
||||
|
@ -32,17 +33,16 @@ pub const VSCODE_CLI_AI_ENDPOINT: Option<&'static str> = option_env!("VSCODE_CLI
|
|||
pub const VSCODE_CLI_QUALITY: Option<&'static str> = option_env!("VSCODE_CLI_QUALITY");
|
||||
pub const DOCUMENTATION_URL: Option<&'static str> = option_env!("VSCODE_CLI_DOCUMENTATION_URL");
|
||||
pub const VSCODE_CLI_COMMIT: Option<&'static str> = option_env!("VSCODE_CLI_COMMIT");
|
||||
pub const VSCODE_CLI_UPDATE_ENDPOINT: Option<&'static str> =
|
||||
option_env!("VSCODE_CLI_UPDATE_ENDPOINT");
|
||||
pub const VSCODE_CLI_UPDATE_ENDPOINT: Option<&'static str> = option_env!("VSCODE_CLI_UPDATE_URL");
|
||||
|
||||
/// Windows lock name for the running tunnel service. Used by the setup script
|
||||
/// to detect a tunnel process. See #179265.
|
||||
pub const TUNNEL_SERVICE_LOCK_NAME: Option<&'static str> =
|
||||
option_env!("VSCODE_CLI_TUNNEL_SERVICE_MUTEX");
|
||||
option_env!("VSCODE_CLI_WIN32_TUNNEL_SERVICE_MUTEX");
|
||||
|
||||
/// Windows lock name for the running tunnel without a service. Used by the setup
|
||||
/// script to detect a tunnel process. See #179265.
|
||||
pub const TUNNEL_CLI_LOCK_NAME: Option<&'static str> = option_env!("VSCODE_CLI_TUNNEL_CLI_MUTEX");
|
||||
pub const TUNNEL_CLI_LOCK_NAME: Option<&'static str> = option_env!("VSCODE_CLI_WIN32_TUNNEL_MUTEX");
|
||||
|
||||
pub const TUNNEL_SERVICE_USER_AGENT_ENV_VAR: &str = "TUNNEL_SERVICE_USER_AGENT";
|
||||
|
||||
|
@ -68,16 +68,24 @@ pub const QUALITYLESS_PRODUCT_NAME: &str = match option_env!("VSCODE_CLI_QUALITY
|
|||
/// Name of the application without quality information.
|
||||
pub const QUALITYLESS_SERVER_NAME: &str = concatcp!(QUALITYLESS_PRODUCT_NAME, " Server");
|
||||
|
||||
pub const QUALITY: &str = match VSCODE_CLI_QUALITY {
|
||||
Some(q) => q,
|
||||
_ => "oss",
|
||||
};
|
||||
|
||||
/// Web URL the editor is hosted at. For VS Code, this is vscode.dev.
|
||||
pub const EDITOR_WEB_URL: Option<&'static str> = option_env!("VSCODE_CLI_EDITOR_WEB_URL");
|
||||
pub const EDITOR_WEB_URL: Option<&'static str> = option_env!("VSCODE_CLI_TUNNEL_EDITOR_WEB_URL");
|
||||
|
||||
/// Name shown in places where we need to tell a user what a process is, e.g. in sleep inhibition.
|
||||
pub const TUNNEL_ACTIVITY_NAME: &str = concatcp!(PRODUCT_NAME_LONG, " Tunnel");
|
||||
|
||||
/// Download URL of the desktop product.
|
||||
pub const PRODUCT_DOWNLOAD_URL: Option<&'static str> = option_env!("VSCODE_CLI_DOWNLOAD_URL");
|
||||
|
||||
const NONINTERACTIVE_VAR: &str = "VSCODE_CLI_NONINTERACTIVE";
|
||||
|
||||
/// Default data CLI data directory.
|
||||
pub const DEFAULT_DATA_PARENT_DIR: &str = match option_env!("VSCODE_CLI_DEFAULT_PARENT_DATA_DIR") {
|
||||
pub const DEFAULT_DATA_PARENT_DIR: &str = match option_env!("VSCODE_CLI_DATA_FOLDER_NAME") {
|
||||
Some(n) => n,
|
||||
None => ".vscode-oss",
|
||||
};
|
||||
|
@ -91,6 +99,12 @@ pub fn get_default_user_agent() -> String {
|
|||
|
||||
const NO_COLOR_ENV: &str = "NO_COLOR";
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ServerQualityInfo {
|
||||
pub server_application_name: String,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref TUNNEL_SERVICE_USER_AGENT: String =
|
||||
match std::env::var(TUNNEL_SERVICE_USER_AGENT_ENV_VAR) {
|
||||
|
@ -98,25 +112,9 @@ lazy_static! {
|
|||
_ => get_default_user_agent(),
|
||||
};
|
||||
|
||||
/// Map of quality names to arrays of app IDs used for them, for example, `{"stable":["ABC123"]}`
|
||||
pub static ref WIN32_APP_IDS: Option<HashMap<Quality, Vec<String>>> =
|
||||
option_env!("VSCODE_CLI_WIN32_APP_IDS").and_then(|s| serde_json::from_str(s).unwrap());
|
||||
|
||||
/// Map of quality names to desktop download URIs
|
||||
pub static ref QUALITY_DOWNLOAD_URIS: Option<HashMap<Quality, String>> =
|
||||
option_env!("VSCODE_CLI_QUALITY_DOWNLOAD_URIS").and_then(|s| serde_json::from_str(s).unwrap());
|
||||
|
||||
/// Map of qualities to the long name of the app in that quality
|
||||
pub static ref PRODUCT_NAME_LONG_MAP: Option<HashMap<Quality, String>> =
|
||||
option_env!("VSCODE_CLI_NAME_LONG_MAP").and_then(|s| serde_json::from_str(s).unwrap());
|
||||
|
||||
/// Map of qualities to the application name
|
||||
pub static ref APPLICATION_NAME_MAP: Option<HashMap<Quality, String>> =
|
||||
option_env!("VSCODE_CLI_APPLICATION_NAME_MAP").and_then(|s| serde_json::from_str(s).unwrap());
|
||||
|
||||
/// Map of qualities to the server name
|
||||
pub static ref SERVER_NAME_MAP: Option<HashMap<Quality, String>> =
|
||||
option_env!("VSCODE_CLI_SERVER_NAME_MAP").and_then(|s| serde_json::from_str(s).unwrap());
|
||||
pub static ref SERVER_NAME_MAP: Option<HashMap<Quality, ServerQualityInfo>> =
|
||||
option_env!("VSCODE_CLI_TUNNEL_SERVER_QUALITIES").and_then(|s| serde_json::from_str(s).unwrap());
|
||||
|
||||
/// Whether i/o interactions are allowed in the current CLI.
|
||||
pub static ref IS_A_TTY: bool = std::io::stdin().is_terminal();
|
||||
|
@ -126,4 +124,8 @@ lazy_static! {
|
|||
|
||||
/// Whether i/o interactions are allowed in the current CLI.
|
||||
pub static ref IS_INTERACTIVE_CLI: bool = *IS_A_TTY && std::env::var(NONINTERACTIVE_VAR).is_err();
|
||||
|
||||
/// Map of quality names to arrays of app IDs used for them, for example, `{"stable":["ABC123"]}`
|
||||
pub static ref WIN32_APP_IDS: Option<Vec<String>> =
|
||||
option_env!("VSCODE_CLI_WIN32_APP_IDS").map(|s| s.split(',').map(|s| s.to_string()).collect());
|
||||
}
|
||||
|
|
|
@ -14,9 +14,8 @@ use regex::Regex;
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
constants::{QUALITYLESS_PRODUCT_NAME, QUALITY_DOWNLOAD_URIS},
|
||||
constants::{PRODUCT_DOWNLOAD_URL, QUALITY, QUALITYLESS_PRODUCT_NAME},
|
||||
log,
|
||||
options::{self, Quality},
|
||||
state::{LauncherPaths, PersistedState},
|
||||
update_service::Platform,
|
||||
util::errors::{AnyError, InvalidRequestedVersion},
|
||||
|
@ -26,34 +25,23 @@ use crate::{
|
|||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[serde(tag = "t", content = "c")]
|
||||
pub enum RequestedVersion {
|
||||
Quality(options::Quality),
|
||||
Version {
|
||||
version: String,
|
||||
quality: options::Quality,
|
||||
},
|
||||
Commit {
|
||||
commit: String,
|
||||
quality: options::Quality,
|
||||
},
|
||||
Default,
|
||||
Commit(String),
|
||||
Path(String),
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref SEMVER_RE: Regex = Regex::new(r"^\d+\.\d+\.\d+(-insider)?$").unwrap();
|
||||
static ref COMMIT_RE: Regex = Regex::new(r"^[a-z]+/[a-e0-f]{40}$").unwrap();
|
||||
static ref COMMIT_RE: Regex = Regex::new(r"^[a-e0-f]{40}$").unwrap();
|
||||
}
|
||||
|
||||
impl RequestedVersion {
|
||||
pub fn get_command(&self) -> String {
|
||||
match self {
|
||||
RequestedVersion::Quality(quality) => {
|
||||
format!("code version use {}", quality.get_machine_name())
|
||||
RequestedVersion::Default => {
|
||||
format!("code version use {}", QUALITY)
|
||||
}
|
||||
RequestedVersion::Version { version, .. } => {
|
||||
format!("code version use {}", version)
|
||||
}
|
||||
RequestedVersion::Commit { commit, quality } => {
|
||||
format!("code version use {}/{}", quality.get_machine_name(), commit)
|
||||
RequestedVersion::Commit(commit) => {
|
||||
format!("code version use {}/{}", QUALITY, commit)
|
||||
}
|
||||
RequestedVersion::Path(path) => {
|
||||
format!("code version use {}", path)
|
||||
|
@ -65,12 +53,11 @@ impl RequestedVersion {
|
|||
impl std::fmt::Display for RequestedVersion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
RequestedVersion::Quality(quality) => write!(f, "{}", quality.get_capitalized_name()),
|
||||
RequestedVersion::Version { version, .. } => {
|
||||
write!(f, "{}", version)
|
||||
RequestedVersion::Default => {
|
||||
write!(f, "{}", QUALITY)
|
||||
}
|
||||
RequestedVersion::Commit { commit, quality } => {
|
||||
write!(f, "{}/{}", quality, commit)
|
||||
RequestedVersion::Commit(commit) => {
|
||||
write!(f, "{}/{}", QUALITY, commit)
|
||||
}
|
||||
RequestedVersion::Path(path) => write!(f, "{}", path),
|
||||
}
|
||||
|
@ -81,19 +68,8 @@ impl TryFrom<&str> for RequestedVersion {
|
|||
type Error = InvalidRequestedVersion;
|
||||
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
if let Ok(quality) = options::Quality::try_from(s) {
|
||||
return Ok(RequestedVersion::Quality(quality));
|
||||
}
|
||||
|
||||
if SEMVER_RE.is_match(s) {
|
||||
return Ok(RequestedVersion::Version {
|
||||
quality: if s.ends_with("-insider") {
|
||||
options::Quality::Insiders
|
||||
} else {
|
||||
options::Quality::Stable
|
||||
},
|
||||
version: s.to_string(),
|
||||
});
|
||||
if s == QUALITY {
|
||||
return Ok(RequestedVersion::Default);
|
||||
}
|
||||
|
||||
if Path::is_absolute(&PathBuf::from(s)) {
|
||||
|
@ -101,13 +77,7 @@ impl TryFrom<&str> for RequestedVersion {
|
|||
}
|
||||
|
||||
if COMMIT_RE.is_match(s) {
|
||||
let idx = s.find('/').expect("expected a /");
|
||||
if let Ok(quality) = options::Quality::try_from(&s[0..idx]) {
|
||||
return Ok(RequestedVersion::Commit {
|
||||
commit: s[idx + 1..].to_string(),
|
||||
quality,
|
||||
});
|
||||
}
|
||||
return Ok(RequestedVersion::Commit(s.to_string()));
|
||||
}
|
||||
|
||||
Err(InvalidRequestedVersion())
|
||||
|
@ -206,7 +176,7 @@ impl CodeVersionManager {
|
|||
.versions
|
||||
.get(stored.current)
|
||||
.map(|(v, _)| v.clone())
|
||||
.unwrap_or(RequestedVersion::Quality(options::Quality::Stable))
|
||||
.unwrap_or(RequestedVersion::Default)
|
||||
}
|
||||
|
||||
/// Tries to get the entrypoint for the version, if one can be found.
|
||||
|
@ -221,7 +191,7 @@ impl CodeVersionManager {
|
|||
|
||||
// For simple quality requests, see if that's installed already on the system
|
||||
let candidates = match &version {
|
||||
RequestedVersion::Quality(q) => match detect_installed_program(&self.log, *q) {
|
||||
RequestedVersion::Default => match detect_installed_program(&self.log) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
warning!(self.log, "error looking up installed applications: {}", e);
|
||||
|
@ -254,8 +224,8 @@ pub fn prompt_to_install(version: &RequestedVersion) {
|
|||
QUALITYLESS_PRODUCT_NAME, version
|
||||
);
|
||||
|
||||
if let RequestedVersion::Quality(quality) = version {
|
||||
if let Some(uri) = QUALITY_DOWNLOAD_URIS.as_ref().and_then(|m| m.get(quality)) {
|
||||
if let RequestedVersion::Default = version {
|
||||
if let Some(uri) = PRODUCT_DOWNLOAD_URL {
|
||||
// todo: on some platforms, we may be able to help automate installation. For example,
|
||||
// we can unzip the app ourselves on macOS and on windows we can download and spawn the GUI installer
|
||||
#[cfg(target_os = "linux")]
|
||||
|
@ -272,11 +242,12 @@ pub fn prompt_to_install(version: &RequestedVersion) {
|
|||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn detect_installed_program(log: &log::Logger, quality: Quality) -> io::Result<Vec<PathBuf>> {
|
||||
fn detect_installed_program(log: &log::Logger) -> io::Result<Vec<PathBuf>> {
|
||||
use crate::constants::PRODUCT_NAME_LONG;
|
||||
|
||||
// easy, fast detection for where apps are usually installed
|
||||
let mut probable = PathBuf::from("/Applications");
|
||||
let app_name = quality.get_long_name();
|
||||
probable.push(format!("{}.app", app_name));
|
||||
probable.push(format!("{}.app", PRODUCT_NAME_LONG));
|
||||
if probable.exists() {
|
||||
probable.extend(["Contents/Resources", "app", "bin", "code"]);
|
||||
return Ok(vec![probable]);
|
||||
|
@ -316,7 +287,7 @@ fn detect_installed_program(log: &log::Logger, quality: Quality) -> io::Result<V
|
|||
line = line.trim();
|
||||
match state {
|
||||
State::LookingForName => {
|
||||
if line.starts_with(app_name) && line.ends_with(':') {
|
||||
if line.starts_with(PRODUCT_NAME_LONG) && line.ends_with(':') {
|
||||
state = State::LookingForLocation;
|
||||
}
|
||||
}
|
||||
|
@ -341,13 +312,13 @@ fn detect_installed_program(log: &log::Logger, quality: Quality) -> io::Result<V
|
|||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn detect_installed_program(_log: &log::Logger, quality: Quality) -> io::Result<Vec<PathBuf>> {
|
||||
use crate::constants::WIN32_APP_IDS;
|
||||
fn detect_installed_program(_log: &log::Logger) -> io::Result<Vec<PathBuf>> {
|
||||
use crate::constants::{APPLICATION_NAME, WIN32_APP_IDS};
|
||||
use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
|
||||
use winreg::RegKey;
|
||||
|
||||
let mut output: Vec<PathBuf> = vec![];
|
||||
let app_ids = match WIN32_APP_IDS.as_ref().and_then(|m| m.get(&quality)) {
|
||||
let app_ids = match WIN32_APP_IDS.as_ref() {
|
||||
Some(ids) => ids,
|
||||
None => return Ok(output),
|
||||
};
|
||||
|
@ -381,7 +352,7 @@ fn detect_installed_program(_log: &log::Logger, quality: Quality) -> io::Result<
|
|||
[
|
||||
location.as_str(),
|
||||
"bin",
|
||||
&format!("{}.cmd", quality.get_application_name()),
|
||||
&format!("{}.cmd", APPLICATION_NAME),
|
||||
]
|
||||
.iter()
|
||||
.collect(),
|
||||
|
@ -397,7 +368,9 @@ fn detect_installed_program(_log: &log::Logger, quality: Quality) -> io::Result<
|
|||
// Looks for the given binary name in the PATH, returning all candidate matches.
|
||||
// Based on https://github.dev/microsoft/vscode-js-debug/blob/7594d05518df6700df51771895fcad0ddc7f92f9/src/common/pathUtils.ts#L15
|
||||
#[cfg(target_os = "linux")]
|
||||
fn detect_installed_program(log: &log::Logger, quality: Quality) -> io::Result<Vec<PathBuf>> {
|
||||
fn detect_installed_program(log: &log::Logger) -> io::Result<Vec<PathBuf>> {
|
||||
use crate::constants::APPLICATION_NAME;
|
||||
|
||||
let path = match std::env::var("PATH") {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
|
@ -406,11 +379,10 @@ fn detect_installed_program(log: &log::Logger, quality: Quality) -> io::Result<V
|
|||
}
|
||||
};
|
||||
|
||||
let name = quality.get_application_name();
|
||||
let current_exe = std::env::current_exe().expect("expected to read current exe");
|
||||
let mut output = vec![];
|
||||
for dir in path.split(':') {
|
||||
let target: PathBuf = [dir, name].iter().collect();
|
||||
let target: PathBuf = [dir, APPLICATION_NAME].iter().collect();
|
||||
match std::fs::canonicalize(&target) {
|
||||
Ok(m) if m == current_exe => continue,
|
||||
Ok(_) => {}
|
||||
|
@ -471,97 +443,40 @@ mod tests {
|
|||
fn test_detect_installed_program() {
|
||||
// developers can run this test and debug output manually; VS Code will not
|
||||
// be installed in CI, so the test only makes sure it doesn't error out
|
||||
let result = detect_installed_program(&log::Logger::test(), Quality::Insiders);
|
||||
let result = detect_installed_program(&log::Logger::test());
|
||||
println!("result: {:?}", result);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_requested_version_parses() {
|
||||
assert_eq!(
|
||||
RequestedVersion::try_from("1.2.3").unwrap(),
|
||||
RequestedVersion::Version {
|
||||
quality: options::Quality::Stable,
|
||||
version: "1.2.3".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
RequestedVersion::try_from("1.2.3-insider").unwrap(),
|
||||
RequestedVersion::Version {
|
||||
quality: options::Quality::Insiders,
|
||||
version: "1.2.3-insider".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
RequestedVersion::try_from("stable").unwrap(),
|
||||
RequestedVersion::Quality(options::Quality::Stable)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
RequestedVersion::try_from("insiders").unwrap(),
|
||||
RequestedVersion::Quality(options::Quality::Insiders)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
RequestedVersion::try_from("insiders/92fd228156aafeb326b23f6604028d342152313b")
|
||||
.unwrap(),
|
||||
RequestedVersion::Commit {
|
||||
commit: "92fd228156aafeb326b23f6604028d342152313b".to_string(),
|
||||
quality: options::Quality::Insiders
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
RequestedVersion::try_from("stable/92fd228156aafeb326b23f6604028d342152313b").unwrap(),
|
||||
RequestedVersion::Commit {
|
||||
commit: "92fd228156aafeb326b23f6604028d342152313b".to_string(),
|
||||
quality: options::Quality::Stable
|
||||
}
|
||||
);
|
||||
|
||||
let exe = std::env::current_exe()
|
||||
.expect("expected to get exe")
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
assert_eq!(
|
||||
RequestedVersion::try_from(exe.as_str()).unwrap(),
|
||||
RequestedVersion::Path(exe),
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_set_preferred_version() {
|
||||
let dir = make_multiple_vscode_install();
|
||||
let lp = LauncherPaths::new_without_replacements(dir.path().to_owned());
|
||||
let vm1 = CodeVersionManager::new(log::Logger::test(), &lp, Platform::LinuxARM64);
|
||||
|
||||
assert_eq!(
|
||||
vm1.get_preferred_version(),
|
||||
RequestedVersion::Quality(options::Quality::Stable)
|
||||
);
|
||||
assert_eq!(vm1.get_preferred_version(), RequestedVersion::Default);
|
||||
vm1.set_preferred_version(
|
||||
RequestedVersion::Quality(options::Quality::Exploration),
|
||||
RequestedVersion::Commit("foobar".to_string()),
|
||||
dir.path().join("desktop/stable"),
|
||||
)
|
||||
.await
|
||||
.expect("expected to store");
|
||||
vm1.set_preferred_version(
|
||||
RequestedVersion::Quality(options::Quality::Insiders),
|
||||
RequestedVersion::Commit("foobar2".to_string()),
|
||||
dir.path().join("desktop/stable"),
|
||||
)
|
||||
.await
|
||||
.expect("expected to store");
|
||||
|
||||
assert_eq!(
|
||||
vm1.get_preferred_version(),
|
||||
RequestedVersion::Quality(options::Quality::Insiders)
|
||||
RequestedVersion::Commit("foobar2".to_string()),
|
||||
);
|
||||
|
||||
let vm2 = CodeVersionManager::new(log::Logger::test(), &lp, Platform::LinuxARM64);
|
||||
assert_eq!(
|
||||
vm2.get_preferred_version(),
|
||||
RequestedVersion::Quality(options::Quality::Insiders)
|
||||
RequestedVersion::Commit("foobar2".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ use std::fmt;
|
|||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::constants::{APPLICATION_NAME_MAP, PRODUCT_NAME_LONG_MAP, SERVER_NAME_MAP};
|
||||
use crate::constants::SERVER_NAME_MAP;
|
||||
|
||||
#[derive(clap::ValueEnum, Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Quality {
|
||||
|
@ -38,30 +38,12 @@ impl Quality {
|
|||
}
|
||||
}
|
||||
|
||||
/// Product long name
|
||||
pub fn get_long_name(&self) -> &'static str {
|
||||
PRODUCT_NAME_LONG_MAP
|
||||
.as_ref()
|
||||
.and_then(|m| m.get(self))
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("Code - OSS")
|
||||
}
|
||||
|
||||
/// Product application name
|
||||
pub fn get_application_name(&self) -> &'static str {
|
||||
APPLICATION_NAME_MAP
|
||||
.as_ref()
|
||||
.and_then(|m| m.get(self))
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or("code")
|
||||
}
|
||||
|
||||
/// Server application name
|
||||
pub fn server_entrypoint(&self) -> String {
|
||||
let mut server_name = SERVER_NAME_MAP
|
||||
.as_ref()
|
||||
.and_then(|m| m.get(self))
|
||||
.map(|s| s.as_str())
|
||||
.map(|s| s.server_application_name.as_str())
|
||||
.unwrap_or("code-server-oss")
|
||||
.to_string();
|
||||
|
||||
|
|
|
@ -6,9 +6,14 @@ use crate::constants::{IS_INTERACTIVE_CLI, PRODUCT_NAME_LONG};
|
|||
use crate::state::{LauncherPaths, PersistedState};
|
||||
use crate::util::errors::{AnyError, MissingLegalConsent};
|
||||
use crate::util::input::prompt_yn;
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const LICENSE_TEXT: Option<&'static str> = option_env!("VSCODE_CLI_REMOTE_LICENSE_TEXT");
|
||||
lazy_static! {
|
||||
static ref LICENSE_TEXT: Option<Vec<String>> =
|
||||
option_env!("VSCODE_CLI_SERVER_LICENSE").and_then(|s| serde_json::from_str(s).unwrap());
|
||||
}
|
||||
|
||||
const LICENSE_PROMPT: Option<&'static str> = option_env!("VSCODE_CLI_REMOTE_LICENSE_PROMPT");
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize)]
|
||||
|
@ -20,8 +25,8 @@ pub fn require_consent(
|
|||
paths: &LauncherPaths,
|
||||
accept_server_license_terms: bool,
|
||||
) -> Result<(), AnyError> {
|
||||
match LICENSE_TEXT {
|
||||
Some(t) => println!("{}", t.replace("\\n", "\r\n")),
|
||||
match &*LICENSE_TEXT {
|
||||
Some(t) => println!("{}", t.join("\r\n")),
|
||||
None => return Ok(()),
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "code-oss-dev",
|
||||
"version": "1.82.0",
|
||||
"distro": "a94daa7ddfb25b11bd8f3376c01e30ff356d0a2b",
|
||||
"distro": "32d09c1ac0a6f84901cd96b1f6f800f3f284fe36",
|
||||
"author": {
|
||||
"name": "Microsoft Corporation"
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue