Skip to content

Commit

Permalink
Remove vscode-dev-containers dependency
Browse files Browse the repository at this point in the history
  • Loading branch information
chrmarti committed Nov 14, 2023
1 parent 7d0d406 commit 25b8c9d
Show file tree
Hide file tree
Showing 14 changed files with 36 additions and 335 deletions.
2 changes: 0 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ The specification repo uses the following [labels](https://github.com/microsoft/

- Create a PR:
- Updating the package version in the `package.json`.
- Updating the `vscode-dev-containers` version in the `package.json`'s dependencies (if there is an update).
- Run `yarn` to update `yarn.lock`.
- List notable changes in the `CHANGELOG.md`.
- Update ThirdPartyNotices.txt with any new dependencies.
- After the PR is merged to `main` wait for the CI workflow to succeed (this builds the artifact that will be published). (TBD: Let the `publish-dev-containers` workflow wait for the CI workflow.)
Expand Down
1 change: 0 additions & 1 deletion esbuild.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ const watch = process.argv.indexOf('--watch') !== -1;
minify,
platform: 'node',
target: 'node14.17.0',
external: ['vscode-dev-containers'],
mainFields: ['module', 'main'],
outdir: 'dist',
plugins: [plugin],
Expand Down
11 changes: 3 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
"node": "^16.13.0 || >=18.0.0"
},
"scripts": {
"compile": "npm-run-all clean-dist definitions compile-dev",
"watch": "npm-run-all clean-dist definitions compile-watch",
"package": "npm-run-all clean-dist definitions compile-prod store-packagejson patch-packagejson npm-pack restore-packagejson",
"compile": "npm-run-all clean-dist compile-dev",
"watch": "npm-run-all clean-dist compile-watch",
"package": "npm-run-all clean-dist compile-prod store-packagejson patch-packagejson npm-pack restore-packagejson",
"store-packagejson": "copyfiles package.json build-tmp/",
"patch-packagejson": "node build/patch-packagejson.js",
"restore-packagejson": "copyfiles --up 1 build-tmp/package.json .",
Expand All @@ -32,10 +32,7 @@
"tsc-b": "tsc -b",
"tsc-b-w": "tsc -b -w",
"precommit": "node build/hygiene.js",
"definitions": "npm-run-all definitions-clean definitions-copy",
"lint": "eslint -c .eslintrc.js --rulesdir ./build/eslint --max-warnings 0 --ext .ts ./src",
"definitions-clean": "rimraf dist/node_modules/vscode-dev-containers",
"definitions-copy": "copyfiles \"node_modules/vscode-dev-containers/container-features/{devcontainer-features.json,feature-scripts.env,fish-debian.sh,homebrew-debian.sh,install.sh}\" dist",
"npm-pack": "npm pack",
"clean": "npm-run-all clean-dist clean-built",
"clean-dist": "rimraf dist",
Expand All @@ -52,7 +49,6 @@
"ThirdPartyNotices.txt",
"devcontainer.js",
"dist/spec-node/devContainersSpecCLI.js",
"dist/node_modules/vscode-dev-containers",
"package.json",
"scripts/updateUID.Dockerfile"
],
Expand Down Expand Up @@ -106,7 +102,6 @@
"stream-to-pull-stream": "^1.7.3",
"tar": "^6.1.13",
"text-table": "^0.2.0",
"vscode-dev-containers": "https://github.com/microsoft/vscode-dev-containers/releases/download/v0.245.2/vscode-dev-containers-0.245.2.tgz",
"vscode-uri": "^3.0.7",
"yargs": "~17.7.1"
}
Expand Down
178 changes: 9 additions & 169 deletions src/spec-configuration/containerFeaturesConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import { getEntPasswdShellCommand } from '../spec-common/commonUtils';

// v1
const V1_ASSET_NAME = 'devcontainer-features.tgz';
export const V1_DEVCONTAINER_FEATURES_FILE_NAME = 'devcontainer-features.json';

// v2
export const DEVCONTAINER_FEATURE_FILE_NAME = 'devcontainer-feature.json';
Expand Down Expand Up @@ -68,7 +67,6 @@ export interface SchemaFeatureBaseProperties {
// Properties that are set programmatically for book-keeping purposes
export interface InternalFeatureProperties {
cachePath?: string;
internalVersion?: string;
consecutiveId?: string;
value: boolean | string | Record<string, boolean | string | undefined>;
currentId?: string;
Expand Down Expand Up @@ -116,18 +114,14 @@ export function parseMount(str: string): Mount {
.reduce((acc, [key, value]) => ({ ...acc, [(normalizedMountKeys[key] || key)]: value }), {}) as Mount;
}

export type SourceInformation = LocalCacheSourceInformation | GithubSourceInformation | DirectTarballSourceInformation | FilePathSourceInformation | OCISourceInformation;
export type SourceInformation = GithubSourceInformation | DirectTarballSourceInformation | FilePathSourceInformation | OCISourceInformation;

interface BaseSourceInformation {
type: string;
userFeatureId: string; // Dictates how a supporting tool will locate and download a given feature. See https://github.com/devcontainers/spec/blob/main/proposals/devcontainer-features.md#referencing-a-feature
userFeatureIdWithoutVersion?: string;
}

export interface LocalCacheSourceInformation extends BaseSourceInformation {
type: 'local-cache';
}

export interface OCISourceInformation extends BaseSourceInformation {
type: 'oci';
featureRef: OCIRef;
Expand Down Expand Up @@ -171,7 +165,6 @@ export interface GithubSourceInformationInput {

export interface FeatureSet {
features: Feature[];
internalVersion?: string;
sourceInformation: SourceInformation;
computedDigest?: string;
}
Expand Down Expand Up @@ -215,18 +208,6 @@ export interface ContainerFeatureInternalParams {
experimentalFrozenLockfile?: boolean;
}

export const multiStageBuildExploration = false;

const isTsnode = path.basename(process.argv[0]) === 'ts-node' || process.argv.indexOf('ts-node/register') !== -1;

export function getContainerFeaturesFolder(_extensionPath: string | { distFolder: string }) {
if (isTsnode) {
return path.join(require.resolve('vscode-dev-containers/package.json'), '..', 'container-features');
}
const distFolder = typeof _extensionPath === 'string' ? path.join(_extensionPath, 'dist') : _extensionPath.distFolder;
return path.join(distFolder, 'node_modules', 'vscode-dev-containers', 'container-features');
}

// TODO: Move to node layer.
export function getContainerFeaturesBaseDockerFile(contentSourceRootPath: string) {
return `
Expand Down Expand Up @@ -329,33 +310,7 @@ echo "_REMOTE_USER_HOME=$(${getEntPasswdShellCommand(remoteUser)} | cut -d: -f6)
`;

// Features version 1
const folders = (featuresConfig.featureSets || []).filter(y => y.internalVersion !== '2').map(x => x.features[0].consecutiveId);
folders.forEach(folder => {
const source = path.posix.join(contentSourceRootPath, folder!);
const dest = path.posix.join(FEATURES_CONTAINER_TEMP_DEST_FOLDER, folder!);
if (!useBuildKitBuildContexts) {
result += `COPY --chown=root:root --from=dev_containers_feature_content_source ${source} ${dest}
RUN chmod -R 0755 ${dest} \\
&& cd ${dest} \\
&& chmod +x ./install.sh \\
&& ./install.sh
`;
} else {
result += `RUN --mount=type=bind,from=dev_containers_feature_content_source,source=${source},target=/tmp/build-features-src/${folder} \\
cp -ar /tmp/build-features-src/${folder} ${FEATURES_CONTAINER_TEMP_DEST_FOLDER} \\
&& chmod -R 0755 ${dest} \\
&& cd ${dest} \\
&& chmod +x ./install.sh \\
&& ./install.sh \\
&& rm -rf ${dest}
`;
}
});
// Features version 2
featuresConfig.featureSets.filter(y => y.internalVersion === '2').forEach(featureSet => {
featuresConfig.featureSets.forEach(featureSet => {
featureSet.features.forEach(feature => {
result += generateContainerEnvs(feature.containerEnv);
const source = path.posix.join(contentSourceRootPath, feature.consecutiveId!);
Expand Down Expand Up @@ -467,29 +422,6 @@ async function askGitHubApiForTarballUri(sourceInformation: GithubSourceInformat
return undefined;
}

export async function loadFeaturesJson(jsonBuffer: Buffer, filePath: string, output: Log): Promise<FeatureSet | undefined> {
if (jsonBuffer.length === 0) {
output.write('Parsed featureSet is empty.', LogLevel.Error);
return undefined;
}

const featureSet: FeatureSet = jsonc.parse(jsonBuffer.toString());
if (!featureSet?.features || featureSet.features.length === 0) {
output.write('Parsed featureSet contains no features.', LogLevel.Error);
return undefined;
}
output.write(`Loaded ${filePath}, which declares ${featureSet.features.length} features and ${(!!featureSet.sourceInformation) ? 'contains' : 'does not contain'} explicit source info.`,
LogLevel.Trace);

return updateFromOldProperties(featureSet);
}

export async function loadV1FeaturesJsonFromDisk(pathToDirectory: string, output: Log): Promise<FeatureSet | undefined> {
const filePath = path.join(pathToDirectory, V1_DEVCONTAINER_FEATURES_FILE_NAME);
const jsonBuffer: Buffer = await readLocalFile(filePath);
return loadFeaturesJson(jsonBuffer, filePath, output);
}

function updateFromOldProperties<T extends { features: (Feature & { extensions?: string[]; settings?: object; customizations?: VSCodeCustomizations })[] }>(original: T): T {
// https://github.com/microsoft/dev-container-spec/issues/1
if (!original.features.find(f => f.extensions || f.settings)) {
Expand Down Expand Up @@ -522,7 +454,7 @@ function updateFromOldProperties<T extends { features: (Feature & { extensions?:

// Generate a base featuresConfig object with the set of locally-cached features,
// as well as downloading and merging in remote feature definitions.
export async function generateFeaturesConfig(params: ContainerFeatureInternalParams, dstFolder: string, config: DevContainerConfig, getLocalFeaturesFolder: (d: string) => string, additionalFeatures: Record<string, string | boolean | Record<string, string | boolean>>) {
export async function generateFeaturesConfig(params: ContainerFeatureInternalParams, dstFolder: string, config: DevContainerConfig, additionalFeatures: Record<string, string | boolean | Record<string, string | boolean>>) {
const { output } = params;

const workspaceRoot = params.cwd;
Expand All @@ -533,15 +465,6 @@ export async function generateFeaturesConfig(params: ContainerFeatureInternalPar
return undefined;
}

// load local cache of features;
// TODO: Update so that cached features are always version 2
const localFeaturesFolder = getLocalFeaturesFolder(params.extensionPath);
const locallyCachedFeatureSet = await loadV1FeaturesJsonFromDisk(localFeaturesFolder, output); // TODO: Pass dist folder instead to also work with the devcontainer.json support package.
if (!locallyCachedFeatureSet) {
output.write('Failed to load locally cached features', LogLevel.Error);
return undefined;
}

let configPath = config.configFilePath && uriToFsPath(config.configFilePath, params.platform);
output.write(`configPath: ${configPath}`, LogLevel.Trace);

Expand All @@ -567,7 +490,7 @@ export async function generateFeaturesConfig(params: ContainerFeatureInternalPar

// Fetch features, stage into the appropriate build folder, and read the feature's devcontainer-feature.json
output.write('--- Fetching User Features ----', LogLevel.Trace);
await fetchFeatures(params, featuresConfig, locallyCachedFeatureSet, dstFolder, localFeaturesFolder, ociCacheDir, lockfile);
await fetchFeatures(params, featuresConfig, dstFolder, ociCacheDir, lockfile);

await logFeatureAdvisories(params, featuresConfig);
await writeLockfile(params, config, featuresConfig, initLockfile);
Expand Down Expand Up @@ -752,11 +675,6 @@ export async function getFeatureIdType(params: CommonParams, userFeatureId: stri
// (1) A feature backed by a GitHub Release
// Syntax: <repoOwner>/<repoName>/<featureId>[@version]

// DEPRECATED: This is a legacy feature-set ID
if (!userFeatureId.includes('/') && !userFeatureId.includes('\\')) {
return { type: 'local-cache', manifest: undefined };
}

// Direct tarball reference
if (userFeatureId.startsWith('https://')) {
return { type: 'direct-tarball', manifest: undefined };
Expand Down Expand Up @@ -834,29 +752,6 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:

const { type, manifest } = await getFeatureIdType(params, userFeature.userFeatureId, lockfile);

// cached feature
// Resolves deprecated features (fish, maven, gradle, homebrew, jupyterlab)
if (type === 'local-cache') {
output.write(`Cached feature found.`);

let feat: Feature = {
id: userFeature.userFeatureId,
name: userFeature.userFeatureId,
value: userFeature.options,
included: true,
};

let newFeaturesSet: FeatureSet = {
sourceInformation: {
type: 'local-cache',
userFeatureId: originalUserFeatureId
},
features: [feat],
};

return newFeaturesSet;
}

// remote tar file
if (type === 'direct-tarball') {
output.write(`Remote tar file found.`);
Expand Down Expand Up @@ -1030,7 +925,7 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
// throw new Error(`Unsupported feature source type: ${type}`);
}

async function fetchFeatures(params: { extensionPath: string; cwd: string; output: Log; env: NodeJS.ProcessEnv }, featuresConfig: FeaturesConfig, localFeatures: FeatureSet, dstFolder: string, localFeaturesFolder: string, ociCacheDir: string, lockfile: Lockfile | undefined) {
async function fetchFeatures(params: { extensionPath: string; cwd: string; output: Log; env: NodeJS.ProcessEnv }, featuresConfig: FeaturesConfig, dstFolder: string, ociCacheDir: string, lockfile: Lockfile | undefined) {
const featureSets = featuresConfig.featureSets;
for (let idx = 0; idx < featureSets.length; idx++) { // Index represents the previously computed installation order.
const featureSet = featureSets[idx];
Expand All @@ -1039,10 +934,6 @@ async function fetchFeatures(params: { extensionPath: string; cwd: string; outpu
continue;
}

if (!localFeatures) {
continue;
}

const { output } = params;

const feature = featureSet.features[0];
Expand Down Expand Up @@ -1071,7 +962,7 @@ async function fetchFeatures(params: { extensionPath: string; cwd: string; outpu
throw new Error(err);
}

if (!(await applyFeatureConfigToFeature(output, featureSet, feature, featCachePath, featureSet.sourceInformation.manifestDigest))) {
if (!(await applyFeatureConfigToFeature(featureSet, feature, featCachePath, featureSet.sourceInformation.manifestDigest))) {
const err = `Failed to parse feature '${featureDebugId}'. Please check your devcontainer.json 'features' attribute.`;
throw new Error(err);
}
Expand All @@ -1080,25 +971,13 @@ async function fetchFeatures(params: { extensionPath: string; cwd: string; outpu
continue;
}

if (sourceInfoType === 'local-cache') {
// create copy of the local features to set the environment variables for them.
await mkdirpLocal(featCachePath);
await cpDirectoryLocal(localFeaturesFolder, featCachePath);

if (!(await applyFeatureConfigToFeature(output, featureSet, feature, featCachePath, undefined))) {
const err = `Failed to parse feature '${featureDebugId}'. Please check your devcontainer.json 'features' attribute.`;
throw new Error(err);
}
continue;
}

if (sourceInfoType === 'file-path') {
output.write(`Detected local file path`, LogLevel.Trace);
await mkdirpLocal(featCachePath);
const executionPath = featureSet.sourceInformation.resolvedFilePath;
await cpDirectoryLocal(executionPath, featCachePath);

if (!(await applyFeatureConfigToFeature(output, featureSet, feature, featCachePath, undefined))) {
if (!(await applyFeatureConfigToFeature(featureSet, feature, featCachePath, undefined))) {
const err = `Failed to parse feature '${featureDebugId}'. Please check your devcontainer.json 'features' attribute.`;
throw new Error(err);
}
Expand Down Expand Up @@ -1146,7 +1025,7 @@ async function fetchFeatures(params: { extensionPath: string; cwd: string; outpu

if (res) {
output.write(`Succeeded fetching ${uri}`, LogLevel.Trace);
if (!(await applyFeatureConfigToFeature(output, featureSet, feature, featCachePath, res.computedDigest))) {
if (!(await applyFeatureConfigToFeature(featureSet, feature, featCachePath, res.computedDigest))) {
const err = `Failed to parse feature '${featureDebugId}'. Please check your devcontainer.json 'features' attribute.`;
throw new Error(err);
}
Expand Down Expand Up @@ -1244,19 +1123,10 @@ export async function fetchContentsAtTarballUri(params: { output: Log; env: Node

// Reads the feature's 'devcontainer-feature.json` and applies any attributes to the in-memory Feature object.
// NOTE:
// Implements the latest ('internalVersion' = '2') parsing logic,
// Falls back to earlier implementation(s) if requirements not present.
// Returns a boolean indicating whether the feature was successfully parsed.
async function applyFeatureConfigToFeature(output: Log, featureSet: FeatureSet, feature: Feature, featCachePath: string, computedDigest: string | undefined): Promise<boolean> {
async function applyFeatureConfigToFeature(featureSet: FeatureSet, feature: Feature, featCachePath: string, computedDigest: string | undefined): Promise<boolean> {
const innerJsonPath = path.join(featCachePath, DEVCONTAINER_FEATURE_FILE_NAME);

if (!(await isLocalFile(innerJsonPath))) {
output.write(`Feature ${feature.id} is not a 'v2' feature. Attempting fallback to 'v1' implementation.`, LogLevel.Trace);
output.write(`For v2, expected devcontainer-feature.json at ${innerJsonPath}`, LogLevel.Trace);
return await parseDevContainerFeature_v1Impl(output, featureSet, feature, featCachePath);
}

featureSet.internalVersion = '2';
featureSet.computedDigest = computedDigest;
feature.cachePath = featCachePath;
const jsonString: Buffer = await readLocalFile(innerJsonPath);
Expand All @@ -1273,36 +1143,6 @@ async function applyFeatureConfigToFeature(output: Log, featureSet: FeatureSet,
return true;
}

async function parseDevContainerFeature_v1Impl(output: Log, featureSet: FeatureSet, feature: Feature, featCachePath: string): Promise<boolean> {

const pathToV1DevContainerFeatureJson = path.join(featCachePath, V1_DEVCONTAINER_FEATURES_FILE_NAME);

if (!(await isLocalFile(pathToV1DevContainerFeatureJson))) {
output.write(`Failed to find ${V1_DEVCONTAINER_FEATURES_FILE_NAME} metadata file (v1)`, LogLevel.Error);
return false;
}
featureSet.internalVersion = '1';
feature.cachePath = featCachePath;
const jsonString: Buffer = await readLocalFile(pathToV1DevContainerFeatureJson);
const featureJson: FeatureSet = jsonc.parse(jsonString.toString());

const seekedFeature = featureJson?.features.find(f => f.id === feature.id);
if (!seekedFeature) {
output.write(`Failed to find feature '${feature.id}' in provided v1 metadata file`, LogLevel.Error);
return false;
}

feature = {
...seekedFeature,
...feature
};

featureSet.features[0] = updateFromOldProperties({ features: [feature] }).features[0];


return true;
}

export function getFeatureMainProperty(feature: Feature) {
return feature.options?.version ? 'version' : undefined;
}
Expand Down
Loading

0 comments on commit 25b8c9d

Please sign in to comment.