diff --git a/.gitattributes b/.gitattributes
index 625449502b..0d63732a99 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1 +1,20 @@
+# Don't allow people to merge changes to these generated files, because the result
+# may be invalid. You need to run "rush update" again.
+ce/pnpm-lock.yaml merge=binary
+ce/shrinkwrap.yaml merge=binary
+ce/npm-shrinkwrap.json merge=binary
+
+
+# Disable line ending smudges entirely.
* -text
+
+# Rush's JSON config files use JavaScript-style code comments. The rule below prevents pedantic
+# syntax highlighters such as GitHub's from highlighting these comments as errors. Your text editor
+# may also require a special configuration to allow comments in JSON.
+#
+# For more information, see this issue: https://github.com/Microsoft/web-build-tools/issues/1088
+#
+*.json linguist-language=JSON-with-Comments
+*.png binary
+*.gif binary
+*.mp4 binary
diff --git a/.gitignore b/.gitignore
index 18cf816fb8..d3738a5bc2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,8 +8,17 @@
/CMakeSettings.json
/out
.DS_Store
-# Qt Creator CMake project files
CMakeLists.txt.user
.cache
/vcpkg-ce.zip
-/vcpkg-ce/**
+node_modules/
+**/.rush/
+**/dist/
+/ce/test/**/*.d.ts
+/ce/test/**/*.map
+/ce/test/**/*.js
+/ce/ce/vcpkg-ce.build.log
+/ce/common/config/rush/pnpm-lock.yaml
+/ce/test/vcpkg-ce.test.build.log
+/ce/common/temp
+/vcpkg-root
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000..f9ba8cf65f
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,9 @@
+# Microsoft Open Source Code of Conduct
+
+This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
+
+Resources:
+
+- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
+- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
+- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
diff --git a/README.md b/README.md
index ae5ec55ddc..2e76bab1f1 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,50 @@ tracking, and edits to which libraries are available.
This repository contains the contents formerly at https://github.com/microsoft/vcpkg in the
"toolsrc" tree, and build support.
+# Vcpkg-ce: "Configure Environment" / artifacts
+
+Parts of vcpkg powered by "ce" are currently in 'preview' -- there will most certainly be changes between now
+and when the tool is 'released' based on feedback.
+
+You can use it, but be forewarned that we may change formats, commands, etc.
+
+Think of it as a manifest-driven desired state configuration for C/C++ projects.
+
+It
+ - integrates itself into your shell (PowerShell, CMD, bash/zsh)
+ - can restore artifacts according to a manifest that follows one’s code
+ - provides discoverability interfaces
+
+## Installation
+
+While the usage of `ce` is the same on all platforms, the installation/loading/removal is slightly different depending on the platform you're using.
+
+`ce` doesn't persist any changes to the environment, nor does it automatically add itself to the start-up environment. If you wish to make it load in a window, you can just execute the script. Manually adding that in your profile will load it in every new window.
+
+
+
+## Install/Use/Remove
+
+| OS | Install | Use | Remove |
+|-----------------|-----------------------------------------------------|-----------------------|---------------------------------|
+| **PowerShell/Pwsh** |`iex (iwr -useb https://aka.ms/vcpkg-init.ps1)` |` . ~/.vcpkg/vcpkg-init.ps1` | `rmdir -recurse ~/.vcpkg` |
+| **Linux/OSX** |`. <(curl https://aka.ms/vcpkg-init.sh -L)` |` . ~/.vcpkg/vcpkg-init.sh` | `rm -rf ~/.ce` |
+| **CMD Shell** |`curl -LO https://aka.ms/vcpkg-init.cmd && .\vcpkg-init.cmd` |`%USERPROFILE%\.vcpkg\vcpkg-init.cmd` | `rmdir /s /q %USERPROFILE%\.vcpkg` |
+
+## Glossary
+
+| Term | Description |
+|------------|-----------------------------------------------------|
+| `artifact` | An archive (.zip or .tar.gz-like), package (.nupkg, .vsix) binary inside which build tools or components thereof are stored. |
+| `artifact metadata` | A description of the locations one or more artifacts describing rules for which ones are deployed given selection of a host architecture, target architecture, or other properties|
+| `artifact identity` | A short string that uniquely describes a moniker that a given artifact (and its metadata) can be referenced by. They can have one of the following forms:
`full/identity/path` - the full identity of an artifact that is in the built-in artifact source
`sourcename:full/identity/path` - the full identity of an artifact that is in the artifact source specified by the sourcename prefix
`shortname` - the shortened unique name of an artifact that is in the built-in artifact source
`sourcename:shortname` - the shortened unique name of an artifact that is in the artifact source specified by the sourcename prefix
Shortened names are generated based off the shortest unique identity path in the given source. |
+| `artifact source` | Also known as a “feed”. An Artifact Source is a location that hosts metadata to locate artifacts. (_There is only one source currently_) |
+| `project profile` | The per-project configuration file (`environment.yaml` or `environment.json`)
+| `AMF` or `Metadata` `Format` | The schema / format of the YAML/JSON files for project profiles, global settings, and artifacts metadata. |
+| `activation` | The process by which a particular set of artifacts are acquired and enabled for use in a calling command program.|
+| `versions` | Version numbers are specified using the Semver format. If a version for a particular operation isn't specified, a range for the latest version ( `*` ) is assumed. A version or version range can be specified using the npm semver matching syntax. When a version is stored, it can be stored using the version range specified, a space and then the version found. (ie, the first version is what was asked for, the second is what was installed. No need for a separate lock file.) |
+
+
# Contributing
Please refer to the "contributing" section of the
@@ -36,6 +80,14 @@ with any additional questions or comments.
The product code in this repository is licensed under the [MIT License](LICENSE.txt). The tests
contain 3rd party code as documented in `NOTICE.txt`.
+# Trademarks
+
+This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft
+trademarks or logos is subject to and must follow
+[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general).
+Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
+Any use of third-party trademarks or logos are subject to those third-party's policies.
+
# Telemetry
vcpkg collects usage data in order to help us improve your experience.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000000..f7b89984f0
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,41 @@
+
+
+## Security
+
+Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
+
+If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below.
+
+## Reporting Security Issues
+
+**Please do not report security vulnerabilities through public GitHub issues.**
+
+Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report).
+
+If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc).
+
+You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
+
+Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
+
+ * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
+ * Full paths of source file(s) related to the manifestation of the issue
+ * The location of the affected source code (tag/branch/commit or direct URL)
+ * Any special configuration required to reproduce the issue
+ * Step-by-step instructions to reproduce the issue
+ * Proof-of-concept or exploit code (if possible)
+ * Impact of the issue, including how an attacker might exploit the issue
+
+This information will help us triage your report more quickly.
+
+If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs.
+
+## Preferred Languages
+
+We prefer all communications to be in English.
+
+## Policy
+
+Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd).
+
+
\ No newline at end of file
diff --git a/azure-pipelines/arch-independent-signing.signproj b/azure-pipelines/arch-independent-signing.signproj
index 338b9ed67a..80a762a42f 100644
--- a/azure-pipelines/arch-independent-signing.signproj
+++ b/azure-pipelines/arch-independent-signing.signproj
@@ -11,14 +11,14 @@
-
+
Microsoft400
-
+
Microsoft400
-
+
3PartyScriptsSHA2
diff --git a/azure-pipelines/pipelines.yml b/azure-pipelines/pipelines.yml
index 428fb23435..5f7ba59312 100644
--- a/azure-pipelines/pipelines.yml
+++ b/azure-pipelines/pipelines.yml
@@ -1,6 +1,6 @@
jobs:
- job: linux_gcc_9
- displayName: 'Ubuntu 20.04 with GCC 9'
+ displayName: 'Ubuntu 20.04 with GCC 9, plus vcpkg-ce'
pool:
vmImage: 'ubuntu-20.04'
variables:
@@ -11,6 +11,34 @@ jobs:
git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT" -n
git -C "$VCPKG_ROOT" checkout `cat vcpkg-init/vcpkg-scripts-sha.txt`
displayName: "Clone vcpkg repo to serve as root"
+ - task: NodeTool@0
+ inputs:
+ versionSpec: '16.x'
+ displayName: 'Install Node.js'
+ - script: |
+ cd ce
+
+ npm install -g npm
+ rc=$?; if [ $rc -ne 0 ]; then exit $rc ; fi
+
+ npx @microsoft/rush update
+ rc=$?; if [ $rc -ne 0 ]; then exit $rc ; fi
+
+ npx @microsoft/rush rebuild
+ rc=$?; if [ $rc -ne 0 ]; then exit $rc ; fi
+
+ npx @microsoft/rush test
+ rc=$?; if [ $rc -ne 0 ]; then exit $rc ; fi
+
+ npx @microsoft/rush lint
+ rc=$?; if [ $rc -ne 0 ]; then exit $rc ; fi
+
+ if [ -n "$(git status --porcelain)" ]; then
+ echo "ERROR: Working directory is dirty. Are there test output files missing from the PR?"
+ git status
+ exit 1
+ fi
+ displayName: 'Rush install, build and test vcpkg-ce'
- bash: |
export CXXFLAGS="-fprofile-arcs -ftest-coverage -fPIC -O0"
cmake -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON -DVCPKG_DEVELOPMENT_WARNINGS=ON -DVCPKG_WARNINGS_AS_ERRORS=ON -DVCPKG_BUILD_FUZZING=ON -B build.amd64.debug
diff --git a/azure-pipelines/signing.yml b/azure-pipelines/signing.yml
index 7bf99c2bf6..39bde73ad8 100644
--- a/azure-pipelines/signing.yml
+++ b/azure-pipelines/signing.yml
@@ -66,55 +66,42 @@ jobs:
filePath: vcpkg-init/lock-versions.ps1
arguments: '-Destination "$(Build.BinariesDirectory)" -VcpkgBaseVersion $(VCPKG_INITIAL_BASE_VERSION)'
# Build and test vcpkg-ce
- - task: PowerShell@2
- displayName: 'Download vcpkg-ce sources'
- inputs:
- targetType: 'inline'
- script: |
- $sha = Get-Content azure-pipelines/vcpkg-ce-sha.txt -Raw
- $sha = $sha.Trim()
- if( test-path vcpkg-ce) { rmdir -recurse -force vcpkg-ce -ea 0 }
- # this will keep the .git folder, which is used by set-versions to accurately set the version of ce
- git clone https://github.com/microsoft/vcpkg-ce/ vcpkg-ce
- cd vcpkg-ce
- git checkout $sha
- pwsh: true
- task: UseNode@1
displayName: Use Node 16 or later
inputs:
version: "16.x"
- script: npm install -g @microsoft/rush
displayName: Install Rush
- workingDirectory: vcpkg-ce
+ workingDirectory: ce
- script: rush update
displayName: Install vcpkg-ce Dependencies
- workingDirectory: vcpkg-ce
+ workingDirectory: ce
- script: rush lint
displayName: Check vcpkg-ce for Linting Errors
- workingDirectory: vcpkg-ce
+ workingDirectory: ce
- script: rush rebuild
displayName: Build vcpkg-ce Packages
- workingDirectory: vcpkg-ce
+ workingDirectory: ce
- script: rush test
displayName: Run vcpkg-ce Tests
- workingDirectory: vcpkg-ce
+ workingDirectory: ce
- script: |
rush set-versions
node -e "const c = require('./ce/package.json'); p = require('./assets/package.json') ; p.version = c.version; require('fs').writeFileSync('./assets/package.json', JSON.stringify(p,undefined,2)); console.log(``set asset version to `${p.version}``);"
displayName: Set vcpkg-ce Package Versions
- workingDirectory: vcpkg-ce
- - script: mkdir "$(Build.BinariesDirectory)\vcpkg-ce" && rush deploy -t "$(Build.BinariesDirectory)\vcpkg-ce"
+ workingDirectory: ce
+ - script: mkdir "$(Build.BinariesDirectory)\ce" && rush deploy -t "$(Build.BinariesDirectory)\ce"
displayName: Collect vcpkg-ce Dependencies
- workingDirectory: vcpkg-ce
+ workingDirectory: ce
- task: ComponentGovernanceComponentDetection@0
displayName: Detect Components
inputs:
- ignoreDirectories: vcpkg-ce/common/temp
+ ignoreDirectories: ce/common/temp
# Inject the NOTICE file. This must run after component detection.
- task: msospo.ospo-extension.8d7f9abb-6896-461d-9e25-4f74ed65ddb2.notice@0
displayName: Generate NOTICE File
inputs:
- outputfile: $(Build.BinariesDirectory)/vcpkg-ce/NOTICE.txt
+ outputfile: $(Build.BinariesDirectory)/ce/NOTICE.txt
- task: MicroBuildSigningPlugin@3
displayName: Install MicroBuild Signing
inputs:
@@ -148,10 +135,10 @@ jobs:
arguments: '-DestinationTarball "$(Build.BinariesDirectory)\vcpkg-standalone-bundle.tar.gz" -TempDir standalone-temp "$(Build.BinariesDirectory)\vcpkg-init.cmd" "$(Build.BinariesDirectory)\vcpkg-init.ps1" "$(Build.BinariesDirectory)\vcpkg-init"'
- script: npm pack
displayName: Create vcpkg-ce Pack
- workingDirectory: $(Build.BinariesDirectory)/vcpkg-ce
+ workingDirectory: $(Build.BinariesDirectory)/ce
- script: |
mkdir "$(Build.ArtifactStagingDirectory)\staging"
- move "$(Build.BinariesDirectory)\vcpkg-ce\vcpkg-ce-*.tgz" "$(Build.ArtifactStagingDirectory)\staging\vcpkg-ce.tgz"
+ move "$(Build.BinariesDirectory)\ce\vcpkg-ce-*.tgz" "$(Build.ArtifactStagingDirectory)\staging\vcpkg-ce.tgz"
move "$(Build.BinariesDirectory)\vcpkg-standalone-bundle.tar.gz" "$(Build.ArtifactStagingDirectory)\staging\vcpkg-standalone-bundle.tar.gz"
move "$(Build.BinariesDirectory)\vcpkg-init" "$(Build.ArtifactStagingDirectory)\staging\vcpkg-init"
move "$(Build.BinariesDirectory)\vcpkg-init.ps1" "$(Build.ArtifactStagingDirectory)\staging\vcpkg-init.ps1"
diff --git a/azure-pipelines/vcpkg-ce-sha.txt b/azure-pipelines/vcpkg-ce-sha.txt
deleted file mode 100644
index d6f8beb8b2..0000000000
--- a/azure-pipelines/vcpkg-ce-sha.txt
+++ /dev/null
@@ -1 +0,0 @@
-b71a8fd4d01cde1ff34c16df2874fa1c763751b6
diff --git a/ce/.eslintignore b/ce/.eslintignore
new file mode 100644
index 0000000000..0f10977a64
--- /dev/null
+++ b/ce/.eslintignore
@@ -0,0 +1 @@
+**/*.d.ts
\ No newline at end of file
diff --git a/ce/.scripts/for-each.js b/ce/.scripts/for-each.js
new file mode 100644
index 0000000000..e238feb342
--- /dev/null
+++ b/ce/.scripts/for-each.js
@@ -0,0 +1,76 @@
+const { spawn } = require('child_process');
+const { readFileSync } = require('fs');
+const { resolve } = require('path');
+
+/** Reads a json/jsonc file and returns the JSON.
+ *
+ * Necessary because rush uses jsonc ( >,< )
+ */
+function read(filename) {
+ const txt = readFileSync(filename, "utf8")
+ .replace(/\r/gm, "")
+ .replace(/\n/gm, "«")
+ .replace(/\/\*.*?\*\//gm, "")
+ .replace(/«/gm, "\n")
+ .replace(/\s+\/\/.*/g, "");
+ return JSON.parse(txt);
+}
+
+const repo = `${__dirname}/..`;
+
+const rush = read(`${repo}/rush.json`);
+const pjs = {};
+
+function forEachProject(onEach) {
+ // load all the projects
+ for (const each of rush.projects) {
+ const packageName = each.packageName;
+ const projectFolder = resolve(`${repo}/${each.projectFolder}`);
+ const project = JSON.parse(readFileSync(`${projectFolder}/package.json`));
+ onEach(packageName, projectFolder, project);
+ }
+}
+
+function npmForEach(cmd) {
+ let count = 0;
+ let failing = false;
+ const result = {};
+ const procs = [];
+ const t1 = process.uptime() * 100;
+ forEachProject((name, location, project) => {
+ // checks for the script first
+ if (project.scripts[cmd]) {
+ count++;
+ const proc = spawn("npm", ["--silent", "run", cmd], { cwd: location, shell: true, stdio: "inherit" });
+ procs.push(proc);
+ result[name] = {
+ name, location, project, proc,
+ };
+ }
+ });
+
+ procs.forEach(proc => proc.on("close", (code, signal) => {
+ count--;
+ failing || !!code;
+
+ if (count === 0) {
+ const t2 = process.uptime() * 100;
+
+ console.log('---------------------------------------------------------');
+ if (failing) {
+ console.log(` Done : command '${cmd}' - ${Math.floor(t2 - t1) / 100} s -- Errors encountered. `)
+ } else {
+ console.log(` Done : command '${cmd}' - ${Math.floor(t2 - t1) / 100} s -- No Errors `)
+ }
+ console.log('---------------------------------------------------------');
+ process.exit(failing ? 1 : 0);
+ }
+ }));
+
+ return result;
+}
+
+module.exports.forEachProject = forEachProject;
+module.exports.npm = npmForEach;
+module.exports.projectCount = rush.projects.length;
+module.exports.read = read;
diff --git a/ce/.scripts/npm-run.js b/ce/.scripts/npm-run.js
new file mode 100644
index 0000000000..ec4511a5ea
--- /dev/null
+++ b/ce/.scripts/npm-run.js
@@ -0,0 +1,2 @@
+// Runs the npm run command on each project that has it.
+require('./for-each').npm(process.argv[2]);
\ No newline at end of file
diff --git a/ce/.scripts/set-versions.js b/ce/.scripts/set-versions.js
new file mode 100644
index 0000000000..03de4048dc
--- /dev/null
+++ b/ce/.scripts/set-versions.js
@@ -0,0 +1,47 @@
+const { exec } = require('child_process');
+const { writeFileSync } = require('fs');
+const { forEachProject, projectCount } = require('./for-each');
+
+let count = projectCount;
+
+
+function updateVersion(name, project, location, patch) {
+ const origJson = JSON.stringify(project, null, 2);
+
+ // update the third digit
+ const verInfo = project.version.split('.');
+ verInfo[2] = patch;
+ project.version = verInfo.join('.');
+
+ // write the file if it's changed
+ const newJson = JSON.stringify(project, null, 2);
+ if (origJson !== newJson) {
+ console.log(`Writing project '${name}' version to '${project.version}' in '${location}'`);
+ writeFileSync(`${location}/package.json`, newJson)
+ }
+
+ count--;
+ if (count === 0) {
+ // last one!
+ // call sync-versions
+ require('./sync-versions');
+ }
+}
+
+if (process.argv[2] === '--reset') {
+ forEachProject((name, location, project) => {
+ updateVersion(name, project, location, 0);
+ })
+} else {
+
+ // Sets the patch version on each package.json in the project.
+ forEachProject((name, location, project) => {
+ if (!process.argv[2] || process.argv[2] === name) {
+ exec(`git rev-list --parents HEAD --count --full-history ..`, { cwd: location }, (o, stdout) => {
+ const patch = (parseInt(stdout.trim()) + (Number(project.patchOffset) || -1));
+ updateVersion(name, project, location, patch);
+ });
+ }
+ });
+}
+
diff --git a/ce/.scripts/sync-versions.js b/ce/.scripts/sync-versions.js
new file mode 100644
index 0000000000..36c26b28b2
--- /dev/null
+++ b/ce/.scripts/sync-versions.js
@@ -0,0 +1,133 @@
+const { readFileSync, writeFileSync } = require('fs');
+const { read } = require('./for-each');
+
+const packageList = {};
+const rush = read(`${__dirname}/../rush.json`);
+const pjs = {};
+
+function writeIfChanged(filename, content) {
+ const orig = JSON.parse(readFileSync(filename))
+ const origJson = JSON.stringify(orig, null, 2);
+ const json = JSON.stringify(content, null, 2);
+
+ if (origJson !== json) {
+ console.log(`Writing updated file '${filename}'`)
+ writeFileSync(filename, json)
+ return true;
+ }
+ return false;
+}
+
+function versionToInt(ver) {
+ let v = ver.replace(/[^\d\.]/g, '').split('.').slice(0, 3);
+ while (v.length < 3) {
+ v.unshift(0);
+ }
+ let n = 0;
+ for (let i = 0; i < v.length; i++) {
+ n = n + ((2 ** (i * 16)) * parseInt(v[v.length - 1 - i]))
+ }
+ return n;
+}
+
+function setPeerDependencies(dependencies) {
+ for (const dep in dependencies) {
+ const ref = pjs[dep];
+ if (ref) {
+ if (dependencies[dep] !== `~${ref.version}`) {
+ console.log(`updating peer depedency ${dep} to ~${ref.version}`);
+ dependencies[dep] = `~${ref.version}`;
+ }
+ }
+ }
+}
+
+
+function recordDeps(dependencies) {
+ for (const packageName in dependencies) {
+ const packageVersion = dependencies[packageName];
+ if (packageList[packageName]) {
+ // same version?
+ if (packageList[packageName] === packageVersion) {
+ continue;
+ }
+ console.log(`${packageName} has ['${packageList[packageName]}','${packageVersion}']`);
+
+ // pick the higher one
+ const v = versionToInt(packageVersion);
+
+ if (v === 0) {
+ console.error(`Unparsed version ${packageName}:${packageVersion}`);
+ process.exit(1);
+ }
+ const v2 = versionToInt(packageList[packageName]);
+ if (v > v2) {
+
+ packageList[packageName] = packageVersion;
+ }
+ } else {
+ packageList[packageName] = packageVersion;
+ }
+ }
+}
+function fixDeps(pj, dependencies) {
+ for (const packageName in dependencies) {
+ if (dependencies[packageName] !== packageList[packageName]) {
+ console.log(`updating ${pj}:${packageName} from '${dependencies[packageName]}' to '${packageList[packageName]}'`)
+ dependencies[packageName] = packageList[packageName];
+ }
+ }
+}
+
+// load all the projects
+for (const each of rush.projects) {
+ const packageName = each.packageName;
+ const projectFolder = each.projectFolder;
+ pjs[packageName] = JSON.parse(readFileSync(`${__dirname}/../${projectFolder}/package.json`));
+}
+
+// verify that peer dependencies are the same version as they are building.
+for (const pj of Object.getOwnPropertyNames(pjs)) {
+ const each = pjs[pj];
+ setPeerDependencies(each.dependencies);
+ setPeerDependencies(each.devDependencies);
+ if (each['static-link']) {
+ setPeerDependencies(each['static-link'].dependencies);
+ }
+}
+
+// now compare to see if someone has an external package with different version
+// than everyone else.
+for (const pj of Object.getOwnPropertyNames(pjs)) {
+ const each = pjs[pj];
+ recordDeps(each.dependencies);
+ recordDeps(each.devDependencies);
+ if (each['static-link']) {
+ recordDeps(each['static-link'].dependencies);
+ }
+}
+
+for (const pj of Object.getOwnPropertyNames(pjs)) {
+ const each = pjs[pj];
+ fixDeps(pj, each.dependencies);
+ fixDeps(pj, each.devDependencies);
+ if (each['static-link']) {
+ fixDeps(pj, each['static-link'].dependencies);
+ }
+}
+var changed = 0;
+
+// write out the results.
+for (const each of rush.projects) {
+ const packageName = each.packageName;
+ const projectFolder = each.projectFolder;
+ if (writeIfChanged(`${__dirname}/../${projectFolder}/package.json`, pjs[packageName])) {
+ changed++;
+ }
+}
+
+if (changed) {
+ console.log(`Updated ${changed} files.`);
+} else {
+ console.log('No changes made')
+}
\ No newline at end of file
diff --git a/ce/.scripts/watch.js b/ce/.scripts/watch.js
new file mode 100644
index 0000000000..cd3e491910
--- /dev/null
+++ b/ce/.scripts/watch.js
@@ -0,0 +1,33 @@
+var cp = require('child_process');
+
+require('./for-each').forEachProject((packageName, projectFolder, project) => {
+ if (project.scripts && project.scripts.watch) {
+ // NOTE: We deliberately use `tsc --watch --project ${projectFolder}` here
+ // with cwd at the repo root instead of `npm run watch` with cwd at project
+ // folder. This ensures that error messages put source file paths relative
+ // to the repo root, which then allows VS Code to navigate to error
+ // locations correctly.
+ const tsc = `${projectFolder}/node_modules/.bin/tsc`;
+ const args = ['--watch', '--project', projectFolder];
+ console.log(`${tsc} ${args.join(' ')}`);
+
+ const proc = cp.spawn(tsc, args, { cwd: `${__dirname}/../`, shell: true, stdio: "inherit" });
+ proc.on("error", (c, s) => {
+ console.log(packageName);
+ console.error(c);
+ console.error(s);
+ });
+ proc.on('exit', (c, s) => {
+ console.log(packageName);
+ console.error(c);
+ console.error(s);
+ });
+ proc.on('message', (c, s) => {
+ console.log(packageName);
+ console.error(c);
+ console.error(s);
+ })
+ }
+});
+
+
diff --git a/ce/assets/LICENSE.txt b/ce/assets/LICENSE.txt
new file mode 100644
index 0000000000..e54fc3fbcb
--- /dev/null
+++ b/ce/assets/LICENSE.txt
@@ -0,0 +1,216 @@
+MICROSOFT PRE-RELEASE SOFTWARE LICENSE TERMS
+
+MICROSOFT VCPKG-CE PROJECT
+
+These license terms are an agreement between Microsoft Corporation (or based on
+where you live, one of its affiliates) and you. They apply to the pre-release
+software named above. The terms also apply to any Microsoft services or updates
+for the software, except to the extent those have additional terms.
+
+IF YOU COMPLY WITH THESE LICENSE TERMS, YOU HAVE THE RIGHTS BELOW.
+
+1. INSTALLATION AND USE RIGHTS.
+
+ a. General. You may install and use any number of copies of the software.
+
+ b. Third Party Components. The software may include third party components
+ with separate legal notices or governed by other agreements, as may be
+ described in the “…Notice” file(s) accompanying the software.
+
+2. PRE-RELEASE SOFTWARE. This software is a pre-release version. It may not
+ operate correctly or work the way a final version will. Microsoft may change
+ it for the final, commercial version. Microsoft is not obligated to provide
+ maintenance, technical support or updates to you for the software.
+
+3. FEEDBACK. If you give feedback about the software to Microsoft, you give to
+ Microsoft, without charge, the right to use, share and commercialize your
+ feedback in any way and for any purpose. You will not give feedback that is
+ subject to a license that requires Microsoft to license its software or
+ documentation to third parties because we include your feedback in them.
+ These rights survive this agreement.
+
+4. DATA.
+
+ a. Data Collection. The software may collect information about you and your
+ use of the software, and send that to Microsoft. Microsoft may use this
+ information to provide services and improve our products and services.
+ You may opt-out of many of these scenarios, but not all, as described in
+ the product documentation. There are also some features in the software
+ that may enable you and Microsoft to collect data from users of your
+ applications. If you use these features, you must comply with applicable
+ law, including providing appropriate notices to users of your
+ applications together with a copy of Microsoft’s privacy statement. Our
+ privacy statement is located at
+ https://go.microsoft.com/fwlink/?LinkID=824704. You can learn more about
+ data collection and use in the help documentation and our privacy
+ statement. Your use of the software operates as your consent to these
+ practices.
+
+ b. Processing of Personal Data. To the extent Microsoft is a processor or
+ subprocessor of personal data in connection with the software, Microsoft
+ makes the commitments in the European Union General Data Protection
+ Regulation Terms of the Online Services Terms to all customers effective
+ May 25, 2018, at https://docs.microsoft.com/en-us/legal/gdpr.
+
+5. TIME-SENSITIVE SOFTWARE.
+
+ a) Period. This agreement is effective on your acceptance and terminates on
+ the earlier of (i) 30 days following first availability of a commercial
+ release of the software or (ii) upon termination by Microsoft. Microsoft
+ may extend this agreement in its discretion.
+
+ b) Notice. You may receive periodic reminder notices of this date through
+ the software.
+
+ c) Access to data. You may not be able to access data used in the software
+ when it stops running.
+
+6. FEEDBACK. If you give feedback about the software to Microsoft, you give to
+ Microsoft, without charge, the right to use, share and commercialize your
+ feedback in any way and for any purpose. You will not give feedback that is
+ subject to a license that requires Microsoft to license its software or
+ documentation to third parties because we include your feedback in them.
+ These rights survive this agreement.
+
+7. SCOPE OF LICENSE. The software is licensed, not sold. This agreement only
+ gives you some rights to use the software. Microsoft reserves all other
+ rights. Unless applicable law gives you more rights despite this limitation,
+ you may use the software only as expressly permitted in this agreement. In
+ doing so, you must comply with any technical limitations in the software
+ that only allow you to use it in certain ways. You may not
+
+ - work around any technical limitations in the software;
+
+ - reverse engineer, decompile or disassemble the software, or otherwise
+ attempt to derive the source code for the software, except and to the
+ extent required by third party licensing terms governing use of certain
+ open-source components that may be included with the software;
+
+ - remove, minimize, block or modify any notices of Microsoft or its
+ suppliers in the software;
+
+ - use the software in any way that is against the law;
+
+ - share, publish, rent, or lease the software; or,
+
+ - provide the software as a stand-alone offering or combined with any of
+ your applications for others to use, or transfer the software or this
+ agreement to any third party.
+
+8. EXPORT RESTRICTIONS. You must comply with all domestic and international
+ export laws and regulations that apply to the software, which include
+ restrictions on destinations, end users and end use. For further information
+ on export restrictions, visit www.microsoft.com/exporting.
+
+9. SUPPORT SERVICES. Because this software is “as is,” we may not provide
+ support services for it.
+
+10. ENTIRE AGREEMENT. This agreement, and the terms for supplements, updates,
+ Internet-based services and support services that you use, are the entire
+ agreement for the software and support services.
+
+11. APPLICABLE LAW. If you acquired the software in the United States,
+ Washington law applies to interpretation of and claims for breach of this
+ agreement, and the laws of the state where you live apply to all other
+ claims. If you acquired the software in any other country, its laws apply.
+
+12. CONSUMER RIGHTS; REGIONAL VARIATIONS. This agreement describes certain legal
+ rights. You may have other rights, including consumer rights, under the laws
+ of your state or country. Separate and apart from your relationship with
+ Microsoft, you may also have rights with respect to the party from which you
+ acquired the software. This agreement does not change those other rights if
+ the laws of your state or country do not permit it to do so. For example, if
+ you acquired the software in one of the below regions, or mandatory country
+ law applies, then the following provisions apply to you:
+
+ a. Australia. You have statutory guarantees under the Australian Consumer
+ Law and nothing in this agreement is intended to affect those rights.
+
+ b. Canada. If you acquired this software in Canada, you may stop receiving
+ updates by turning off the automatic update feature, disconnecting your
+ device from the Internet (if and when you re-connect to the Internet,
+ however, the software will resume checking for and installing updates),
+ or uninstalling the software. The product documentation, if any, may also
+ specify how to turn off updates for your specific device or software.
+
+ c. Germany and Austria.
+
+ (i) Warranty. The properly licensed software will perform substantially
+ as described in any Microsoft materials that accompany the software.
+ However, Microsoft gives no contractual guarantee in relation to the
+ licensed software.
+
+ (ii) Limitation of Liability. In case of intentional conduct, gross
+ negligence, claims based on the Product Liability Act, as well as,
+ in case of death or personal or physical injury, Microsoft is liable
+ according to the statutory law.
+
+ Subject to the foregoing clause (ii), Microsoft will only be liable for
+ slight negligence if Microsoft is in breach of such material contractual
+ obligations, the fulfillment of which facilitate the due performance of
+ this agreement, the breach of which would endanger the purpose of this
+ agreement and the compliance with which a party may constantly trust in
+ (so-called "cardinal obligations"). In other cases of slight negligence,
+ Microsoft will not be liable for slight negligence.
+
+13. DISCLAIMER OF WARRANTY. THE SOFTWARE IS LICENSED “AS-IS.” YOU BEAR THE RISK
+ OF USING IT. MICROSOFT GIVES NO EXPRESS WARRANTIES, GUARANTEES OR
+ CONDITIONS. TO THE EXTENT PERMITTED UNDER YOUR LOCAL LAWS, MICROSOFT
+ XCLUDES THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+ PURPOSE AND NON-INFRINGEMENT.
+
+14. LIMITATION ON AND EXCLUSION OF DAMAGES. YOU CAN RECOVER FROM MICROSOFT AND
+ ITS SUPPLIERS ONLY DIRECT DAMAGES UP TO U.S. $5.00. YOU CANNOT RECOVER ANY
+ OTHER DAMAGES, INCLUDING CONSEQUENTIAL, LOST PROFITS, SPECIAL, INDIRECT OR
+ INCIDENTAL DAMAGES.
+
+ This limitation applies to (a) anything related to the software, services,
+ content (including code) on third party Internet sites, or third party
+ applications; and (b) claims for breach of contract, breach of warranty,
+ guarantee or condition, strict liability, negligence, or other tort to the
+ extent permitted by applicable law.
+
+ It also applies even if Microsoft knew or should have known about the
+ possibility of the damages. The above limitation or exclusion may not apply
+ to you because your country may not allow the exclusion or limitation of
+ incidental, consequential or other damages.
+
+Please note: As this software is distributed in Quebec, Canada, some of the
+clauses in this agreement are provided below in French.
+
+Remarque : Ce logiciel étant distribué au Québec, Canada, certaines des clauses
+dans ce contrat sont fournies ci-dessous en français.
+
+EXONÉRATION DE GARANTIE. Le logiciel visé par une licence est offert
+« tel quel ». Toute utilisation de ce logiciel est à votre seule risque et
+péril. Microsoft n’accorde aucune autre garantie expresse. Vous pouvez
+bénéficier de droits additionnels en vertu du droit local sur la protection des
+consommateurs, que ce contrat ne peut modifier. La ou elles sont permises par le
+droit locale, les garanties implicites de qualité marchande, d’adéquation à un
+usage particulier et d’absence de contrefaçon sont exclues.
+
+LIMITATION DES DOMMAGES-INTÉRÊTS ET EXCLUSION DE RESPONSABILITÉ POUR LES
+DOMMAGES. Vous pouvez obtenir de Microsoft et de ses fournisseurs une
+indemnisation en cas de dommages directs uniquement à hauteur de 5,00 $ US. Vous
+ne pouvez prétendre à aucune indemnisation pour les autres dommages, y compris
+les dommages spéciaux, indirects ou accessoires et pertes de bénéfices.
+
+Cette limitation concerne:
+
+- tout ce qui est relié au logiciel, aux services ou au contenu (y compris le
+ code) figurant sur des sites Internet tiers ou dans des programmes tiers ; et
+
+- les réclamations au titre de violation de contrat ou de garantie, ou au titre
+ de responsabilité stricte, de négligence ou d’une autre faute dans la limite
+ autorisée par la loi en vigueur.
+
+Elle s’applique également, même si Microsoft connaissait ou devrait connaître
+l’éventualité d’un tel dommage. Si votre pays n’autorise pas l’exclusion ou la
+limitation de responsabilité pour les dommages indirects, accessoires ou de
+quelque nature que ce soit, il se peut que la limitation ou l’exclusion
+ci-dessus ne s’appliquera pas à votre égard.
+
+EFFET JURIDIQUE. Le présent contrat décrit certains droits juridiques. Vous
+pourriez avoir d’autres droits prévus par les lois de votre pays. Le présent
+contrat ne modifie pas les droits que vous confèrent les lois de votre pays si
+celles-ci ne le permettent pas.
diff --git a/ce/assets/NOTICE.txt b/ce/assets/NOTICE.txt
new file mode 100644
index 0000000000..d3f5a12faa
--- /dev/null
+++ b/ce/assets/NOTICE.txt
@@ -0,0 +1 @@
+
diff --git a/ce/assets/package.json b/ce/assets/package.json
new file mode 100644
index 0000000000..03cf13c83b
--- /dev/null
+++ b/ce/assets/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "vcpkg-ce",
+ "version": "0.7.0",
+ "description": "vcpkg-ce CLI",
+ "main": "ce/dist/main.js",
+ "bin": {
+ "ce_": "./ce/dist/main.js"
+ },
+ "directories": {
+ "doc": "docs"
+ },
+ "engines": {
+ "node": ">=14.17.0"
+ },
+ "scripts": {
+ "postinstall": "node ./create-links create && node ./wrapper-scripts create",
+ "uninstall": "node ./create-links remove && node ./wrapper-scripts remove",
+ "prepack": "npx rimraf ./common/temp/node_modules/.pnpm/typescript* ./common/temp/node_modules/.pnpm/translate-strings* ./common/temp/node_modules/.pnpm/ts-morph* ./common/temp/node_modules/.pnpm/@types* && node ./prepare-deploy.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/Microsoft/vcpkg-tool.git"
+ },
+ "files": [
+ "**/*"
+ ],
+ "keywords": [
+ "vcpkg-ce",
+ "vcpkg",
+ "ce"
+ ],
+ "author": "Microsoft",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/Microsoft/vcpkg/issues"
+ },
+ "homepage": "https://github.com/Microsoft/vcpkg#readme",
+ "readme": "https://github.com/Microsoft/vcpkg/blob/master/readme.md"
+}
diff --git a/ce/assets/prepare-deploy.js b/ce/assets/prepare-deploy.js
new file mode 100644
index 0000000000..24260ec3b9
--- /dev/null
+++ b/ce/assets/prepare-deploy.js
@@ -0,0 +1,10 @@
+const { unlinkSync, writeFileSync } = require('fs');
+const { join } = require('path');
+
+const file = join(__dirname, 'deploy-metadata.json');
+const metadata = require(file);
+
+const links = metadata.links;
+metadata.links = links.filter(link => link.kind !== 'folderLink' || (link.linkPath.indexOf('/@types') === -1 && link.targetPath.indexOf('/typescript@') === -1));
+writeFileSync(file, JSON.stringify(metadata, null, 2));
+unlinkSync(__filename);
\ No newline at end of file
diff --git a/ce/assets/scripts/ce b/ce/assets/scripts/ce
new file mode 100644
index 0000000000..dc4d7cd6db
--- /dev/null
+++ b/ce/assets/scripts/ce
@@ -0,0 +1,481 @@
+#!/bin/sh
+
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+# wrapper script for ce.
+# this is intended to be dot-sourced and then you can use the ce() function.
+
+# DEBUGGING :
+# set | grep -i ^VCPKG
+
+# check to see if we've been dot-sourced (should work for most POSIX shells)
+sourced=0
+
+if [ -n "$ZSH_EVAL_CONTEXT" ]; then
+ case $ZSH_EVAL_CONTEXT in *:file) sourced=1;; esac
+elif [ -n "$KSH_VERSION" ]; then
+ [ "$(cd $(dirname -- $0) && pwd -P)/$(basename -- $0)" != "$(cd $(dirname -- ${.sh.file}) && pwd -P)/$(basename -- ${.sh.file})" ] && sourced=1
+elif [ -n "$BASH_VERSION" ]; then
+ (return 0 2>/dev/null) && sourced=1
+else # All other shells: examine $0 for known shell binary filenames
+ # Detects `sh` and `dash`; add additional shell filenames as needed.
+ case ${0##*/} in sh|dash) sourced=1;; esac
+fi
+
+if [ $sourced -eq 0 ]; then
+ echo 'This script is expected to be dot-sourced so that it may load ce into the'
+ echo 'current environment and not require permanent changes to the system when you activate.'
+ echo ''
+ echo "You should instead run '. $(basename $0)' first to import ce into the current session."
+ exit
+fi
+
+# GLOBALS
+VCPKG_NODE_LATEST=16.12.0
+VCPKG_NODE_REMOTE=https://nodejs.org/dist/
+VCPKG_PWD=`pwd`
+
+VCPKG_init() {
+ VCPKG_OS="$(uname | sed 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/')"
+ VCPKG_ARCH="$(uname -m | sed -e 's/x86_64/x64/;s/i86pc/x64/;s/i686/x86/;s/aarch64/arm64/')"
+
+ case $VCPKG_OS in
+ ( mingw64_nt* | msys_nt* ) VCPKG_OS="win";;
+ ( darwin*|*bsd*) VCPKG_IS_THIS_BSD=TRUE;;
+ ( aix ) VCPKG_ARCH="ppc64" ;;
+ esac
+
+ if [ ! -z "$VCPKG_IS_THIS_BSD" ]; then
+ VCPKG_START_TIME=$(date +%s)
+ else
+ VCPKG_START_TIME=$(($(date +%s%N)/1000000))
+ fi
+
+ # find important cmdline args
+ VCPKG_ARGS=()
+ for each in "$@"; do case $each in
+ --reset-ce) VCPKG_RESET=TRUE;;
+ --remove-ce) VCPKG_REMOVE=TRUE;;
+ --debug) VCPKG_DEBUG=TRUE && VCPKG_ARGS+=("$each");;
+ *) VCPKG_ARGS+=("$each");;
+ esac ;done
+}
+
+VCPKG_init "$@"
+shift $#
+
+# at this point, we're pretty sure we've been dot-sourced
+# Try to locate the $VCPKG_ROOT folder, where the ce is installed.
+if [ -n "$VCPKG_ROOT" ]; then
+ # specify it on the command line if you like, we'll export it
+ export VCPKG_ROOT=$VCPKG_ROOT
+else
+ # default is off the home folder
+ export VCPKG_ROOT=~/.vcpkg
+fi;
+
+CE=${VCPKG_ROOT}
+mkdir -p $CE
+
+VCPKG_DOWNLOADS=${CE}/downloads
+
+VCPKG_NODE=${CE}/downloads/bin/node
+VCPKG_NPM=${CE}/bin/npm
+
+VCPKG_debug() {
+ if [ ! -z "$VCPKG_IS_THIS_BSD" ]; then
+ local NOW=$(date +%s)
+ local OFFSET="$(( $NOW - $VCPKG_START_TIME )) sec"
+ else
+ local NOW=$(($(date +%s%N)/1000000))
+ local OFFSET="$(( $NOW - $VCPKG_START_TIME )) msec"
+ fi
+
+ if [ ! -z "$VCPKG_DEBUG" ]; then
+ if [ -n "$VCPKG_NESTED" ]; then
+ echo "[NESTED $OFFSET] $*"
+ else
+ echo "[$OFFSET] $*"
+ fi
+ fi
+
+ if [ -n "$VCPKG_NESTED" ]; then
+ echo "[NESTED $OFFSET] $*" >> $VCPKG_ROOT/log.txt
+ else
+ echo "[$OFFSET] $*" >> $VCPKG_ROOT/log.txt
+ fi
+}
+
+if [ ! -z "$VCPKG_RESET" ]; then
+ if [ -d $CE/node_modules ]; then
+ echo Forcing reinstall of vcpkg-ce package
+ rm -rf $CE/node_modules
+ fi
+
+ if [ -d $CE/bin ]; then
+ rm -rf $CE/bin
+ fi
+
+ if [ -d $CE/lib ]; then
+ rm -rf $CE/lib
+ fi
+fi
+
+if [ ! -z "$VCPKG_REMOVE" ]; then
+ if [ -d $CE/node_modules ]; then
+ echo Removing vcpkg-ce package
+ rm -rf $CE/node_modules
+ fi
+
+ if [ -d $CE/bin ]; then
+ rm -rf $CE/bin
+ fi
+
+ if [ -d $CE/lib ]; then
+ rm -rf $CE/lib
+ fi
+
+ if [ -f $VCPKG_ROOT/ce.ps1 ]; then
+ rm -f $VCPKG_ROOT/ce.ps1
+ fi
+
+ if [ -f $VCPKG_ROOT/ce ]; then
+ rm -f $VCPKG_ROOT/ce
+ fi
+
+ if [ -f $VCPKG_ROOT/NOTICE.txt ]; then
+ rm -f $VCPKG_ROOT/NOTICE.txt
+ fi
+
+ if [ -f $VCPKG_ROOT/LICENSE.txt ]; then
+ rm -f $VCPKG_ROOT/LICENSE.txt
+ fi
+
+ # cleanup environment.
+ . <(set | grep -i ^VCPKG | sed -e 's/[=| |\W].*//;s/^/unset /')
+
+ # remove functions (zsh)
+ which functions > /dev/null 2>&1 && . <(functions | grep -i ^vcpkg | sed -e 's/[=| |\W].*//;s/^/unset -f /')
+ return
+fi
+
+VCPKG_cleanup() {
+ # clear things that we're not going to need for the long term
+ unset VCPKG_NODE_LATEST
+ unset VCPKG_NODE_REMOTE
+ unset VCPKG_PWD
+ unset VCPKG_NPM
+ unset VCPKG_NESTED
+ unset VCPKG_OS
+ unset VCPKG_ARCH
+ unset VCPKG_IS_THIS_BSD
+ unset VCPKG_DEBUG
+ unset -f VCPKG_bootstrap_node > /dev/null 2>&1
+ unset -f VCPKG_bootstrap_ce > /dev/null 2>&1
+ unset VCPKG_REMOVE
+ unset VCPKG_RESET
+ unset VCPKG_START_TIME
+ unset VCPKG_ARGS
+ if [ -f "${Z_VCPKG_POSTSCRIPT}" ]; then
+ command rm "${Z_VCPKG_POSTSCRIPT}"
+ fi
+ unset Z_VCPKG_POSTSCRIPT
+}
+VCPKG_verify_node() {
+ # $1 should be the folder to check
+ local NODE_EXE="node"
+ if [ "${VCPKG_OS}" = "win" ]; then
+ NODE_EXE="node.exe"
+ fi
+
+ local N=$(which $1/$NODE_EXE)
+
+ if [ ! -z "$N" ]; then
+ if [ -f $N ]; then
+ if [ $($N -e "[major, minor, patch ] = process.versions.node.split('.'); console.log( !!(major>16 || major == 16 & minor >= 12) )") = "true" ]; then
+ VCPKG_NODE=$N
+ VCPKG_NPM=$(which $1/npm)
+ VCPKG_debug using node in $1
+ return 0
+ fi
+ fi
+ fi
+ return 1;
+}
+
+VCPKG_find_node() {
+ local NODES=$(find $1 | grep -i /bin/node)
+
+ for each in $NODES; do
+ local d=$(dirname "$each")
+ VCPKG_verify_node $d
+ if [ $? -eq 0 ]; then
+ return 0;
+ fi
+ done
+ return 1;
+}
+
+VCPKG_bootstrap_node() {
+ VCPKG_debug starting VCPKG_bootstrap_node
+
+ # did we put one in downloads at some point?
+ VCPKG_find_node $VCPKG_DOWNLOADS
+ if [ $? -eq 0 ]; then
+ return 0;
+ fi
+
+ # is there one on the path?
+ VCPKG_find_node $(dirname $(which node))
+ if [ $? -eq 0 ]; then
+ return 0;
+ fi
+
+ local NODE_EXE="node"
+ if [ "${VCPKG_OS}" = "win" ]; then
+ NODE_EXE="node.exe"
+ fi
+
+ # we don't seem to have a suitable nodejs on the path
+ # let's grab a well-known one, cache it and use it.
+
+ local VCPKG_ARCHIVE_EXT=".tar.gz"
+ local TAR_FLAGS="-zxvf"
+ if [ "${VCPKG_OS}" = "win" ]; then
+ VCPKG_ARCHIVE_EXT=".zip"
+ fi
+
+ local NODE_FULLNAME="node-v${VCPKG_NODE_LATEST}-${VCPKG_OS}-${VCPKG_ARCH}"
+ local NODE_URI="${VCPKG_NODE_REMOTE}v${VCPKG_NODE_LATEST}/${NODE_FULLNAME}${VCPKG_ARCHIVE_EXT}"
+ local VCPKG_ARCHIVE="${VCPKG_DOWNLOADS}/${NODE_FULLNAME}${VCPKG_ARCHIVE_EXT}"
+
+ if [ ! -d "${VCPKG_DOWNLOADS}" ]; then
+ command mkdir -p "${VCPKG_DOWNLOADS}"
+ fi
+
+ echo "Downloading node from ${NODE_URI} to ${VCPKG_ARCHIVE}"
+ if type noglob > /dev/null 2>&1; then
+ noglob curl -L -# "${NODE_URI}" -o "${VCPKG_ARCHIVE}"
+ else
+ curl -L -# "${NODE_URI}" -o "${VCPKG_ARCHIVE}"
+ fi
+
+ if [ ! -f "${VCPKG_ARCHIVE}" ]; then
+ echo "Failed to download node binary."
+ return 1
+ fi
+
+ # UNPACK IT
+ if [ "${VCPKG_OS}" = "aix" ]; then
+ gunzip "${VCPKG_ARCHIVE}" | tar -xvC "${VCPKG_DOWNLOADS}" "${NODE_FULLNAME}/bin/${NODE_EXE}" >> $VCPKG_ROOT/log.txt 2>&1
+ else
+ tar $TAR_FLAGS "${VCPKG_ARCHIVE}" -C "${VCPKG_DOWNLOADS}" >> $VCPKG_ROOT/log.txt 2>&1
+ fi
+
+ # OK, we good?
+ VCPKG_find_node $VCPKG_DOWNLOADS
+ if [ $? -eq 0 ]; then
+ return 0;
+ fi
+
+ if [ ! -f $VCPKG_NPM ]; then
+ echo "ERROR! Unable to find/get npm"
+ return 1;
+ fi
+
+ VCPKG_debug installed node in ce
+ return 0
+}
+
+VCPKG_SCRIPT=${CE}/node_modules/.bin/ce
+VCPKG_MAIN=${CE}/node_modules/vcpkg-ce
+
+VCPKG_bootstrap_ce() {
+ VCPKG_debug checking for installed ce $VCPKG_SCRIPT
+
+ if [ -f $VCPKG_SCRIPT ]; then
+ VCPKG_debug ce is installed.
+ return 0
+ fi
+
+ # it's not there!
+ # let's install it where we want it
+
+ # ensure we have a node_modules here, so npm won't search for one up the tree.
+ command mkdir -p $CE/node_modules
+
+ echo Installing vcpkg-ce in $VCPKG_ROOT
+ unset VCPKG_RESET
+
+ cd $CE
+ $VCPKG_NODE $VCPKG_NPM cache clean --force >> $VCPKG_ROOT/log.txt 2>&1
+ local OLD_PATH=$PATH
+ PATH=`dirname $VCPKG_NODE`:$PATH
+ if [ ! -z "$USE_LOCAL_VCPKG_PKG" ]; then
+ echo USING LOCAL CE PACKAGE $USE_LOCAL_VCPKG_PKG
+
+ $VCPKG_NODE $VCPKG_NPM --force install --no-save --no-lockfile --scripts-prepend-node-path=true $USE_LOCAL_VCPKG_PKG >> $VCPKG_ROOT/log.txt 2>&1
+ else
+ $VCPKG_NODE $VCPKG_NPM --force install --no-save --no-lockfile --scripts-prepend-node-path=true https://aka.ms/vcpkg-ce.tgz >> $VCPKG_ROOT/log.txt 2>&1
+ fi
+
+ PATH=$OLD_PATH
+# go back where we were
+ cd $VCPKG_PWD
+
+ cp $CE/node_modules/.bin/ce* $VCPKG_ROOT/
+
+ # Copy the NOTICE and LICENSE files to $VCPKG_ROOT to improve discoverability.
+ cp $CE/node_modules/vcpkg-ce/NOTICE.txt $VCPKG_ROOT/
+ cp $CE/node_modules/vcpkg-ce/LICENSE.txt $VCPKG_ROOT/
+
+ if [ ! -f $VCPKG_SCRIPT ]; then
+ echo "ERROR! Unable to find/get ce script command $VCPKG_SCRIPT"
+ return 1;
+ fi
+
+ VCPKG_debug ce is installed
+ return 0;
+}
+
+# first, let's make sure we have a good copy of node
+VCPKG_bootstrap_node
+
+if [ $? -eq 1 ]; then
+ VCPKG_debug failed to acquire node.js
+ VCPKG_cleanup
+ return 1;
+fi
+
+VCPKG_bootstrap_ce
+
+# is ce installed?
+if [ $? -eq 1 ]; then
+ VCPKG_debug failed to bootstrap ce
+ VCPKG_cleanup
+ return 1;
+fi
+
+if [ -z $VCPKG_NESTED ]; then
+ VCPKG_debug executing final script: $VCPKG_SCRIPT
+ VCPKG_NESTED=TRUE
+ . $VCPKG_SCRIPT $VCPKG_ARGS
+ return # let the real script take over from here.
+fi
+
+# So, we're the real script then.
+VCPKG_debug 'real ce adding function'
+
+ce() {
+ # set | grep -i ^VCPKG
+
+ local cst=$VCPKG_START_TIME
+
+ VCPKG_init "$@"
+
+ if [ ! -z "$VCPKG_RESET" ]; then
+ if [ -d $CE/node_modules ]; then
+ echo Forcing reinstall of vcpkg-ce package
+ rm -rf $CE/node_modules
+ fi
+
+ if [ -d $CE/bin ]; then
+ rm -rf $CE/bin
+ fi
+
+ if [ -d $CE/lib ]; then
+ rm -rf $CE/lib
+ fi
+ unset VCPKG_RESET
+
+ if [ ! -z "$USE_LOCAL_VCPKG_SCRIPT" ]; then
+ echo USING LOCAL CE SCRIPT $USE_LOCAL_VCPKG_SCRIPT
+ . <(cat $USE_LOCAL_VCPKG_SCRIPT) "${VCPKG_ARGS[@]}"
+ else
+ . <(curl -L -# aka.ms/install-ce.sh) "${VCPKG_ARGS[@]}"
+ fi
+
+ return 0
+ fi
+
+ if [ ! -z "$VCPKG_REMOVE" ]; then
+ if [ -d $CE/node_modules ]; then
+ unset VCPKG_REMOVE
+ echo Removing vcpkg-ce package
+ rm -rf $CE/node_modules
+ fi
+
+ if [ -d $CE/bin ]; then
+ rm -rf $CE/bin
+ fi
+
+ if [ -d $CE/lib ]; then
+ rm -rf $CE/lib
+ fi
+
+ if [ -f $VCPKG_ROOT/ce.ps1 ]; then
+ rm -f $VCPKG_ROOT/ce.ps1
+ fi
+
+ if [ -f $VCPKG_ROOT/ce ]; then
+ rm -f $VCPKG_ROOT/ce
+ fi
+
+ if [ -f $VCPKG_ROOT/NOTICE.txt ]; then
+ rm -f $VCPKG_ROOT/NOTICE.txt
+ fi
+
+ if [ -f $VCPKG_ROOT/LICENSE.txt ]; then
+ rm -f $VCPKG_ROOT/LICENSE.txt
+ fi
+
+ # cleanup environment
+ . <(set | grep -i ^VCPKG | sed -e 's/[=| |\W].*//;s/^/unset /')
+
+ # remove functions (zsh)
+ which functions > /dev/null 2>&1 && . <(functions | grep -i ^vcpkg | sed -e 's/[=| |\W].*//;s/^/unset -f /')
+ return 0
+ fi
+
+ if [ ! -f $VCPKG_NODE ]; then
+ echo The installation of nodejs $VCPKG_NODE that ce is using is missing
+ echo You may need to reacquire ce with '. <(curl aka.ms/install-ce.sh -L)'
+ echo or fix your nodejs installation.
+ fi
+
+ if [ ! -d $VCPKG_MAIN ]; then
+ echo The installation of ce is corrupted. $VCPKG_MAIN
+ echo You may need to reacquire ce with '. <(curl aka.ms/install-ce.sh -L)'
+ fi
+
+ # set the response file
+ # Generate 32 bits of randomness, to avoid clashing with concurrent executions.
+ export Z_VCPKG_POSTSCRIPT="${VCPKG_ROOT}/VCPKG_tmp_$(dd if=/dev/urandom count=1 2> /dev/null | cksum | cut -f1 -d" ").sh"
+
+ # call ce.js
+ # it picks up the Z_VCPKG_POSTSCRIPT environment variable to know where to dump the postscript
+ $VCPKG_NODE --harmony $VCPKG_MAIN ${VCPKG_ARGS[@]}
+
+ VCPKG_debug called ce.js
+ # modify the environment
+
+ # Call the post-invocation script if it is present, then delete it.
+ # This allows the invocation to potentially modify the caller's environment (e.g. PATH)
+ if [ -f "${Z_VCPKG_POSTSCRIPT}" ]; then
+ . "${Z_VCPKG_POSTSCRIPT}"
+ command rm "${Z_VCPKG_POSTSCRIPT}"
+ unset Z_VCPKG_POSTSCRIPT
+ fi
+
+ VCPKG_cleanup
+
+ VCPKG_START_TIME=$cst
+}
+
+# did they dotsource and have args go ahead and run it then!
+if [ -n "$VCPKG_ARGS" ]; then
+ ce "${VCPKG_ARGS[@]}"
+fi
+
+VCPKG_cleanup
diff --git a/ce/assets/scripts/ce.ps1 b/ce/assets/scripts/ce.ps1
new file mode 100644
index 0000000000..c1eac5894e
--- /dev/null
+++ b/ce/assets/scripts/ce.ps1
@@ -0,0 +1,452 @@
+@(echo off) > $null
+if #ftw NEQ '' goto :init
+($true){ $Error.clear(); }
+
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+# wrapper script for ce.
+# this is intended to be dot-sourced and then you can use the ce() function
+
+# unpack arguments if they came from CMD
+$hash=@{};
+get-item env:argz* |% { $hash[$_.name] = $_.value }
+if ($hash.count -gt 0) {
+ $args=for ($i=0; $i -lt $hash.count;$i++) { $hash["ARGZ[$i]"] }
+}
+# force the array to be an arraylist since we like to mutate it.
+$args=[System.Collections.ArrayList][System.Array]$args
+
+# GLOBALS
+$VCPKG_NODE_LATEST='16.12.0'
+$VCPKG_NODE_REMOTE='https://nodejs.org/dist/'
+$VCPKG_START_TIME=get-date
+
+function resolve([string]$name) {
+ $name = Resolve-Path $name -ErrorAction 0 -ErrorVariable _err
+ if (-not($name)) { return $_err[0].TargetObject }
+ $Error.clear()
+ return $name
+}
+
+$SCRIPT:DEBUG=( $args.indexOf('--debug') -gt -1 )
+
+function ce-debug() {
+ $t = [int32]((get-date).Subtract(($VCPKG_START_TIME)).ticks/10000)
+ if($SCRIPT:DEBUG) {
+ write-host -fore green "[$t msec] " -nonewline
+ write-host -fore gray $args
+ }
+ write-output "[$t msec] $args" >> $VCPKG_ROOT/log.txt
+}
+
+function download($url, $path) {
+ $wc = New-Object net.webclient
+
+ if( test-path -ea 0 $path) {
+ # check to see if the size is a match before downloading
+ $s = $wc.OpenRead($url)
+ $len = $wc.ResponseHeaders['Content-Length']
+ $s.Dispose()
+ if( (get-item $path).Length -eq $len ){
+ $wc.Dispose();
+ ce-debug "skipping download of '$url' - '$path' is ok."
+ return $path;
+ }
+ }
+ ce-debug "Downloading '$url' -> '$path'"
+ $wc.DownloadFile($url, $path);
+ $wc.Dispose();
+ if( (get-item $path).Length -ne $wc.ResponseHeaders['Content-Length'] ) {
+ throw "Download of '$url' failed. Check your internet connection."
+ }
+ ce-debug "Completed Download of $url"
+ return $path
+}
+
+# set the home path.
+if( $ENV:VCPKG_ROOT ) {
+ $SCRIPT:VCPKG_ROOT=(resolve $ENV:VCPKG_ROOT)
+ $ENV:VCPKG_ROOT=$VCPKG_ROOT
+} else {
+ $SCRIPT:VCPKG_ROOT=(resolve "$HOME/.vcpkg")
+ $ENV:VCPKG_ROOT=$VCPKG_ROOT
+}
+
+# set the download path
+if( $ENV:VCPKG_DOWNLOADS ) {
+ $SCRIPT:VCPKG_DOWNLOADS= (resolve $ENV:VCPKG_DOWNLOADS)
+ $ENV:VCPKG_DOWNLOADS=$VCPKG_DOWNLOADS
+} else {
+ $SCRIPT:VCPKG_DOWNLOADS= (resolve "$VCPKG_ROOT/downloads")
+ $ENV:VCPKG_DOWNLOADS=$VCPKG_DOWNLOADS
+}
+
+$CE = "${VCPKG_ROOT}"
+$MODULES= "$CE/node_modules"
+
+$SCRIPT:VCPKG_SCRIPT=(resolve $MODULES/.bin/ce.ps1)
+$SCRIPT:CE_MODULE=(resolve $MODULES/vcpkg-ce )
+
+$reset = $args.IndexOf('--reset-ce') -gt -1
+$remove = $args.IndexOf('--remove-ce') -gt -1
+
+if( $reset -or -$remove ) {
+ $args.remove('--reset-ce');
+ $args.remove('--remove-ce');
+
+ if( $reset ) {
+ write-host "Resetting vcpkg-ce"
+ }
+
+ remove-item -recurse -force -ea 0 "$MODULES/.bin","$MODULES"
+ remove-item -force -ea 0 "${VCPKG_ROOT}/ce.ps1","${VCPKG_ROOT}/ce.cmd","${VCPKG_ROOT}/ce","${VCPKG_ROOT}/NOTICE.txt","${VCPKG_ROOT}/LICENSE.txt"
+ $error.clear();
+
+ if( $remove ) {
+ write-host "Removing vcpkg-ce"
+ exit
+ }
+}
+
+function verify-node($NODE) {
+ if( $NODE -and (get-command -ea 0 $NODE) -and ( (& $NODE -p "/(^\d*\.\d*)/g.exec( process.versions.node)[0]") -ge 16.12 ) ) {
+ # it's a good version of node, let's set the variables
+ $SCRIPT:VCPKG_NODE=$NODE
+ $error.clear();
+ return $TRUE;
+ }
+ $error.clear();
+ return $FALSE
+}
+
+function find-node() {
+ $PLACES= @($VCPKG_DOWNLOADS,"$ENV:LOCALAPPDATA/vcpkg/downloads/tools")
+ for( $i=0; $i -lt $PLACES.count; $i++ ) {
+ $p = $PLACES[$i]
+ if( $p ) {
+ $NODES= @()+((get-childitem -ea 0 $p -recurse |? {$_.name -in @('node.exe', 'node')}).FullName)
+ for( $j=0; $j -lt $NODES.count; $j++ ) {
+ $NODE=$NODES[$j]
+ if( verify-node $NODE ) {
+ return $NODE
+ }
+ }
+ }
+ }
+}
+
+function bootstrap-node {
+ # if we have a custom ce node let's use that first
+ $NODE=find-node
+ if( $NODE ) {
+ ce-debug "Node: $NODE"
+ return $TRUE;
+ }
+
+ # check the node on the path.
+ if( (verify-node ((get-command node -ea 0).source ))) {
+ ce-debug "Node: ${VCPKG_NODE}"
+ return $TRUE;
+ }
+
+ # not there, or not good enough
+ if((($PSVersionTable.OS -match "windows") -or ($PSVersionTable.PSEdition -match 'desktop') ) ) { # windows
+ $NODE_OS='win'
+ switch($ENV:PROCESSOR_ARCHITECTURE) {
+ 'AMD64' { $NODE_ARCH='x64' }
+ 'ARM64' { $NODE_ARCH='arm64' }
+ Default { $NODE_ARCH='x86' }
+ }
+ $NODE_ARCHIVE_EXT=".zip"
+ } else {
+ $NODE_OS=(uname | sed 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/')
+ $NODE_ARCH=(uname -m | sed -e 's/x86_64/x64/;s/i86pc/x64/;s/i686/x86/;s/aarch64/arm64/')
+ if ( $NODE_OS -eq "aix" ) { $NODE_ARCH="ppc64" } #aix special
+ $NODE_ARCHIVE_EXT=".tar.gz"
+ }
+
+ $NODE_FULLNAME="node-v${VCPKG_NODE_LATEST}-${NODE_OS}-${NODE_ARCH}"
+ $NODE_URI="${VCPKG_NODE_REMOTE}v${VCPKG_NODE_LATEST}/${NODE_FULLNAME}${NODE_ARCHIVE_EXT}"
+ $NODE_FOLDER=resolve "${VCPKG_DOWNLOADS}/${NODE_FULLNAME}"
+ $NODE_ARCHIVE=resolve "$VCPKG_DOWNLOADS/${NODE_FULLNAME}${NODE_ARCHIVE_EXT}"
+
+ write-host "Installing node runtime"
+
+ $ProgressPreference = 'SilentlyContinue'
+ ce-debug "Downloading Node: ${NODE_URI}"
+ download $NODE_URI $NODE_ARCHIVE
+ $shh = new-item -type directory -ea 0 $NODE_FOLDER
+
+ switch($NODE_OS){
+ 'win' {
+ if( get-command -ea 0 tar.exe ) {
+ tar "-xvf" "${NODE_ARCHIVE}" -C "${NODE_FOLDER}" 2>&1 > $null
+ } else {
+ $shh= expand-archive -path $NODE_ARCHIVE -destinationpath "$NODE_FOLDER"
+ }
+ }
+ 'aix' {
+ $shh = gunzip "${NODE_ARCHIVE}" | tar -xvC "$NODE_FOLDER" "${NODE_FULLNAME}/bin/"
+ }
+ default {
+ $shh = tar "-zxvf" "${NODE_ARCHIVE}" -C "$NODE_FOLDER"
+ }
+ }
+
+ $NODE=find-node
+ if( $NODE ) {
+ ce-debug "Node: $NODE"
+ return $TRUE;
+ }
+
+ write-error 'Unable to resolve nodejs'
+ return $FALSE;
+}
+
+function bootstrap-vcpkg-ce {
+ if(test-path "${ce}/ce.ps1") {
+ if( test-path $VCPKG_SCRIPT ) {
+ return $TRUE;
+ }
+
+ ## if we're running from an installed module location, we'll keep that.
+ $MODULE=(resolve ${PSScriptRoot}/node_modules/vcpkg-ce )
+
+ if( test-path $MODULE ) {
+ $SCRIPT:CE_MODULE=$MODULE
+ return $TRUE
+ }
+ }
+
+ # cleanup the yarn cache.
+ ce-debug "Clearing YARN cache"
+ $shh = & $VCPKG_NODE $YARN cache clean --force 2>&1
+ $error.clear();
+
+ write-host "Installing vcpkg-ce to ${VCPKG_ROOT}"
+
+ if( $ENV:USE_LOCAL_VCPKG_PKG ) {
+ $USE_LOCAL_VCPKG_PKG=$ENV:USE_LOCAL_VCPKG_PKG
+ }
+
+ $PKG = $USE_LOCAL_VCPKG_PKG
+ if( -not $PKG ) {
+ $PKG = 'https://aka.ms/vcpkg-ce.tgz'
+ }
+ pushd $CE
+
+ $PATH = $ENV:PATH
+ $N_DIR=(resolve "$VCPKG_NODE/..")
+ $ENV:PATH="$N_DIR;$PATH"
+
+ &$VCPKG_NODE $YARN add $PKG --no-lockfile --force --scripts-prepend-node-path=true --modules-folder=$MODULES 2>&1 >> $VCPKG_ROOT/log.txt
+ $ENV:PATH = $PATH
+
+ remove-item -path $ce/package.json -ea 0
+
+ popd
+
+ ce-debug 'yarn finished.'
+ if( $error.count -gt 0 ) {
+ $error |% { add-content -encoding UTF8 $VCPKG_ROOT/log.txt $_ }
+ $Error.clear()
+ }
+
+ # we should also copy the .bin files into the $VCPKG_ROOT folder to make reactivation (without being on the PATH) easy
+ copy-item "$MODULES/.bin/ce*" $VCPKG_ROOT
+
+ # Copy the NOTICE and LICENSE files to $VCPKG_ROOT to improve discoverability.
+ copy-item "$CE_MODULE/NOTICE.txt","$CE_MODULE/LICENSE.txt" $VCPKG_ROOT
+
+ ce-debug "Bootstrapped vcpkg-ce: ${VCPKG_ROOT}"
+
+ if( -not (test-path $CE_MODULE )) {
+ write-error "ERROR! Unable to find/get vcpkg-ce module $CE_MODULE"
+ return $false;
+ }
+ return $true;
+}
+
+# ensure it's there.
+$shh = new-item -type directory $CE,$MODULES,"$CE/scripts",$VCPKG_DOWNLOADS -ea 0
+
+# grab the yarn cli script
+$SCRIPT:YARN = resolve "$CE/scripts/yarn.js"
+if( -not (test-path $SCRIPT:YARN )) {
+ $SCRIPT:YARN = download https://aka.ms/yarn.js $YARN
+}
+
+if( -not (bootstrap-node )) {
+ write-error "Unable to acquire an appropriate version of Node."
+ write-error "You should install the LTS version or greater of NodeJS"
+ throw "Installation Unsuccessful."
+}
+
+if( -not (bootstrap-vcpkg-ce )) {
+ write-error "Unable to install vcpkg-ce."
+ throw "Installation Unsuccessful."
+}
+
+# export vcpkg-ce to the current shell.
+$shh = New-Module -name vcpkg-ce -ArgumentList @($VCPKG_NODE,$CE_MODULE,$VCPKG_ROOT) -ScriptBlock {
+ param($VCPKG_NODE,$CE_MODULE,$VCPKG_ROOT)
+
+ function resolve([string]$name) {
+ $name = Resolve-Path $name -ErrorAction 0 -ErrorVariable _err
+ if (-not($name)) { return $_err[0].TargetObject }
+ $Error.clear()
+ return $name
+ }
+
+ function ce() {
+ if( ($args.indexOf('--remove-ce') -gt -1) -or ($args.indexOf('--reset-ce') -gt -1)) {
+ # we really want to do call the ps1 script to do this.
+ if( test-path "${VCPKG_ROOT}/ce.ps1" ) {
+ & "${VCPKG_ROOT}/ce.ps1" @args
+ }
+ return
+ }
+
+ if( -not (test-path $CE_MODULE )) {
+ write-error "vcpkg-ce is not installed."
+ write-host -nonewline "You can reinstall vcpkg-ce by running "
+ write-host -fore green "iex (iwr -useb aka.ms/install-ce.ps1)"
+ return
+ }
+
+ # setup the postscript file
+ # Generate 31 bits of randomness, to avoid clashing with concurrent executions.
+ $env:Z_VCPKG_POSTSCRIPT = resolve "${VCPKG_ROOT}/VCPKG_tmp_$(Get-Random -SetSeed $PID).ps1"
+
+ & $VCPKG_NODE --harmony $CE_MODULE @args
+
+ # dot-source the postscript file to modify the environment
+ if ($env:Z_VCPKG_POSTSCRIPT -and (Test-Path $env:Z_VCPKG_POSTSCRIPT)) {
+ # write-host (get-content -raw $env:Z_VCPKG_POSTSCRIPT)
+ $postscr = get-content -raw $env:Z_VCPKG_POSTSCRIPT
+ if( $postscr ) {
+ iex $postscr
+ }
+ Remove-Item -Force -ea 0 $env:Z_VCPKG_POSTSCRIPT,env:Z_VCPKG_POSTSCRIPT
+ }
+ }
+}
+
+# finally, if this was run with some arguments, then let's just pass it
+if( $args.length -gt 0 ) {
+ ce @args
+}
+
+return
+<#
+:set
+set ARGZ[%i%]=%1&set /a i+=1 & goto :eof
+
+:unset
+set %1=& goto :eof
+
+:init
+if exist $null erase $null
+
+:: do anything we need to before calling into powershell
+if exist $null erase $null
+
+IF "%VCPKG_ROOT%"=="" SET VCPKG_ROOT=%USERPROFILE%\.vcpkg
+
+if exist %~dp0ce\node_modules\vcpkg-ce\package.json (
+ :: we're running the wrapper script for a module-installed vcpkg-ce
+ set VCPKG_CMD=%~dpf0
+ set VCPKG_MODULE=%~dp0ce\node_modules\vcpkg-ce
+ goto INVOKE
+)
+
+:: we're running vcpkg-ce from the ce home folder
+set VCPKG_CMD=%VCPKG_ROOT%\ce\node_modules\vcpkg-ce\ce.cmd
+
+:: if we're being asked to reset the install, call bootstrap
+if "%1" EQU "--reset-ce" goto BOOTSTRAP
+
+:: if we're being asked to remove the install, call bootstrap
+if "%1" EQU "--remove-ce" (
+ set REMOVE_CE=TRUE
+ doskey ce=
+ goto BOOTSTRAP
+)
+
+:: do we even have it installed?
+if NOT exist "%VCPKG_CMD%" goto BOOTSTRAP
+
+set VCPKG_MODULE="%VCPKG_ROOT%\ce\node_modules\vcpkg-ce"
+
+:: if this is the actual installed vcpkg-ce, let's get to the invocation
+if "%~dfp0" == "%VCPKG_CMD%" goto INVOKE
+
+:: this is not the 'right' ce cmd, let's forward this on to that one.
+call %VCPKG_CMD% %*
+set VCPKG_EXITCODE=%ERRORLEVEL%
+goto :eof
+
+:INVOKE
+:: Generate 30 bits of randomness, to avoid clashing with concurrent executions.
+SET /A Z_VCPKG_POSTSCRIPT=%RANDOM% * 32768 + %RANDOM%
+SET Z_VCPKG_POSTSCRIPT=%VCPKG_ROOT%\VCPKG_tmp_%Z_VCPKG_POSTSCRIPT%.cmd
+
+:: find the right node
+if exist %VCPKG_ROOT%\ce\bin\node.exe set VCPKG_NODE=%VCPKG_ROOT%\ce\bin\node.exe
+if "%VCPKG_NODE%" EQU "" (
+ for %%i in (node.exe) do set VCPKG_NODE=%%~$PATH:i
+)
+if "%VCPKG_NODE%" EQU "" goto OHNONONODE:
+
+:: call the program
+"%VCPKG_NODE%" --harmony "%VCPKG_MODULE%" %*
+set VCPKG_EXITCODE=%ERRORLEVEL%
+doskey ce="%VCPKG_CMD%" $*
+
+:POSTSCRIPT
+:: Call the post-invocation script if it is present, then delete it.
+:: This allows the invocation to potentially modify the caller's environment (e.g. PATH).
+IF NOT EXIST "%Z_VCPKG_POSTSCRIPT%" GOTO :fin
+CALL "%Z_VCPKG_POSTSCRIPT%"
+DEL "%Z_VCPKG_POSTSCRIPT%"
+
+goto :fin
+
+:OHNONONODE
+set VCPKG_EXITCODE=1
+echo "Unable to find the nodejs for ce to run."
+goto fin:
+
+:BOOTSTRAP
+:: add the cmdline args to the environment so powershell can use them
+set /a i=0 & for %%a in (%*) do call :set %%a
+
+set POWERSHELL_EXE=
+for %%i in (pwsh.exe powershell.exe) do (
+ if EXIST "%%~$PATH:i" set POWERSHELL_EXE=%%~$PATH:i & goto :gotpwsh
+)
+:gotpwsh
+
+"%POWERSHELL_EXE%" -noprofile -executionpolicy unrestricted -command "iex (get-content %~dfp0 -raw)#" && set REMOVE_CE=
+set VCPKG_EXITCODE=%ERRORLEVEL%
+
+:: clear out the argz
+@for /f "delims==" %%_ in ('set ^| findstr -i argz') do call :unset %%_
+
+:: if we're being asked to remove it,we're done.
+if "%REMOVE_CE%" EQU "TRUE" (
+ goto :fin
+)
+
+:CREATEALIAS
+doskey ce="%VCPKG_ROOT%\ce.cmd" $*
+
+:fin
+SET Z_VCPKG_POSTSCRIPT=
+SET VCPKG_CMD=
+set VCPKG_NODE=
+
+EXIT /B %VCPKG_EXITCODE%
+goto :eof
+#>
\ No newline at end of file
diff --git a/ce/assets/wrapper-scripts.js b/ce/assets/wrapper-scripts.js
new file mode 100644
index 0000000000..68ea2b3c6a
--- /dev/null
+++ b/ce/assets/wrapper-scripts.js
@@ -0,0 +1,112 @@
+const { existsSync: exists, chmod, chmodSync } = require('fs');
+const { stat, copyFile, unlink } = require('fs').promises;
+const { join } = require('path');
+
+/**
+ * This script creates/removes custom wrapper scripts for vcpkg-ce.
+ */
+async function findScriptFolder() {
+ const root = `${__dirname}`;
+ let s = root;
+ while (true) {
+ s = join(s, '..');
+
+ // did we find a folder where the script is in the folder (windows style)
+ if (exists(s) && (await stat(s)).isDirectory() && (
+ exists(join(s, 'ce_.ps1')) ||
+ exists(join(s, 'ce_.cmd')) ||
+ exists(join(s, 'ce.ps1')) ||
+ exists(join(s, 'ce.cmd')))
+ ) {
+ return s;
+ }
+
+ // find it in a bin folder
+ for (const f of ['.bin', 'bin']) {
+ const b1 = join(s, f);
+ if (exists(b1) && (await stat(b1)).isDirectory() && (
+ exists(join(b1, 'ce_')) ||
+ exists(join(b1, 'ce')) ||
+ exists(join(b1, 'ce.ps1')) ||
+ exists(join(b1, 'ce_.ps1')))
+ ) {
+ return b1;
+ }
+ }
+
+ if (s === join(s, '..')) {
+ return undefined;
+ }
+ }
+}
+
+async function create() {
+ const folder = await findScriptFolder();
+ if (!folder) {
+ console.error("Unable to find install'd folder. Aborting.")
+ return process.exit(1);
+ }
+ const files = {
+ 'ce': {
+ source: 'ce',
+ install: process.platform !== 'win32'
+ },
+ 'ce.ps1': {
+ source: 'ce.ps1',
+ install: true
+ },
+ 'ce.cmd': {
+ source: 'ce.ps1',
+ install: process.platform === 'win32'
+ }
+ }
+
+ for (const file of ['ce_', 'ce_.ps1', 'ce_.cmd']) {
+ // remove the normally created scripts
+ const target = join(folder, file);
+ if (exists(target)) {
+ await unlink(target);
+ }
+ }
+
+ // we install all of these, because an installation from bash can still work with powershell
+ for (const file of Object.keys(files)) {
+ console.log(`file: ${file} <== ${files[file].source} if ${files[file].install}`)
+ if (files[file].install) {
+ const target = join(folder, file);
+
+ // remove the symlink/script file if it exists
+ if (exists(target)) {
+ await unlink(target);
+ }
+ // copy the shell script into it's place
+ console.log(`copyFile: ${join(__dirname, "scripts", files[file].source)} ==> ${target} }`);
+ await copyFile(join(__dirname, "scripts", files[file].source), target);
+
+ chmodSync(target, 0o765);
+ }
+ }
+}
+
+async function remove() {
+ const folder = await findScriptFolder();
+ if (!folder) {
+ return process.exit(0);
+ }
+
+ for (const file of ['ce', 'ce.ps1', 'ce.cmd']) {
+ // remove the custom created scripts
+ const target = join(folder, file);
+ if (exists(target)) {
+ await unlink(target);
+ }
+ }
+}
+
+if (process.argv[2] !== 'remove') {
+ console.error('Installing Scripts');
+ create();
+} else {
+ console.error('After this is uninstalled, you should close this terminal.');
+ remove()
+}
diff --git a/ce/ce.ps1 b/ce/ce.ps1
new file mode 100644
index 0000000000..733a73eaa7
--- /dev/null
+++ b/ce/ce.ps1
@@ -0,0 +1,40 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+$ENV:NODE_OPTIONS="--enable-source-maps"
+
+
+function resolve {
+ param ( [string] $name )
+ $name = Resolve-Path $name -ErrorAction 0 -ErrorVariable _err
+ if (-not($name)) { return $_err[0].TargetObject }
+ $Error.clear()
+ return $name
+}
+
+
+if( $ENV:VCPKG_ROOT ) {
+ $SCRIPT:VCPKG_ROOT=(resolve $ENV:VCPKG_ROOT)
+ $ENV:VCPKG_ROOT=$VCPKG_ROOT
+} else {
+ $SCRIPT:VCPKG_ROOT=(resolve "$HOME/.vcpkg")
+ $ENV:VCPKG_ROOT=$VCPKG_ROOT
+}
+
+# setup the postscript file
+# Generate 31 bits of randomness, to avoid clashing with concurrent executions.
+$env:Z_VCPKG_POSTSCRIPT = resolve "${VCPKG_ROOT}/VCPKG_tmp_$(Get-Random -SetSeed $PID).ps1"
+
+node $PSScriptRoot/ce @args
+
+# dot-source the postscript file to modify the environment
+if ($env:Z_VCPKG_POSTSCRIPT -and (Test-Path $env:Z_VCPKG_POSTSCRIPT)) {
+ # write-host (get-content -raw $env:Z_VCPKG_POSTSCRIPT)
+ $content = get-content -raw $env:Z_VCPKG_POSTSCRIPT
+
+ if( $content ) {
+ iex $content
+ }
+ Remove-Item -Force $env:Z_VCPKG_POSTSCRIPT
+ remove-item -ea 0 -force env:Z_VCPKG_POSTSCRIPT
+}
diff --git a/ce/ce/.eslintignore b/ce/ce/.eslintignore
new file mode 100644
index 0000000000..cc6a7fd139
--- /dev/null
+++ b/ce/ce/.eslintignore
@@ -0,0 +1,3 @@
+**/*.d.ts
+test/scenarios/**
+dist/**
diff --git a/ce/ce/.eslintrc.yaml b/ce/ce/.eslintrc.yaml
new file mode 100644
index 0000000000..0609828702
--- /dev/null
+++ b/ce/ce/.eslintrc.yaml
@@ -0,0 +1,10 @@
+---
+# configure plugins first
+parser: "@typescript-eslint/parser"
+plugins:
+- "@typescript-eslint"
+- "notice"
+
+# then inherit the common settings
+extends:
+- "../common/.default-eslintrc.yaml"
\ No newline at end of file
diff --git a/ce/ce/.npmignore b/ce/ce/.npmignore
new file mode 100644
index 0000000000..33986ae1f9
--- /dev/null
+++ b/ce/ce/.npmignore
@@ -0,0 +1,20 @@
+!dist/**/*
+src/
+dist/test/
+package/
+.npmignore
+tsconfig.json
+*.ts
+changelog.md
+.eslint*
+!*.d.ts
+*.tgz
+.vscode
+.scripts
+attic/
+generated/
+notes.md
+Examples/
+samples/
+*.log
+package-deps.json
diff --git a/ce/ce/LICENSE b/ce/ce/LICENSE
new file mode 100644
index 0000000000..5cf7c8db62
--- /dev/null
+++ b/ce/ce/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) Microsoft Corporation. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE
diff --git a/ce/ce/amf/Requires.ts b/ce/ce/amf/Requires.ts
new file mode 100644
index 0000000000..ae40b5c225
--- /dev/null
+++ b/ce/ce/amf/Requires.ts
@@ -0,0 +1,33 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { Scalar } from 'yaml';
+import { VersionReference as IVersionReference } from '../interfaces/metadata/version-reference';
+import { CustomScalarMap } from '../yaml/CustomScalarMap';
+import { Yaml, YAMLDictionary } from '../yaml/yaml-types';
+import { VersionReference } from './version-reference';
+
+export class Requires extends CustomScalarMap {
+ constructor(node?: YAMLDictionary, parent?: Yaml, key?: string) {
+ super(VersionReference, node, parent, key);
+ }
+
+ override set(key: string, value: VersionReference | IVersionReference | string) {
+ if (typeof value === 'string') {
+ this.assert(true); // if we don't have a node at the moment, we need to create one.
+ this.node.set(key, new Scalar(value));
+ return;
+ }
+ if (value.raw) {
+ this.assert(true); // if we don't have a node at the moment, we need to create one.
+ this.node.set(key, new Scalar(value.raw));
+ }
+ if (value.resolved) {
+ this.assert(true); // if we don't have a node at the moment, we need to create one.
+ this.node.set(key, new Scalar(`${value.range} ${value.resolved}`));
+ } else {
+ this.assert(true); // if we don't have a node at the moment, we need to create one.
+ this.node.set(key, new Scalar(value.range));
+ }
+ }
+}
\ No newline at end of file
diff --git a/ce/ce/amf/contact.ts b/ce/ce/amf/contact.ts
new file mode 100644
index 0000000000..c296a73b69
--- /dev/null
+++ b/ce/ce/amf/contact.ts
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { Dictionary } from '../interfaces/collections';
+import { Contact as IContact } from '../interfaces/metadata/contact';
+import { ValidationError } from '../interfaces/validation-error';
+import { Entity } from '../yaml/Entity';
+import { EntityMap } from '../yaml/EntityMap';
+import { Strings } from '../yaml/strings';
+import { Yaml, YAMLDictionary } from '../yaml/yaml-types';
+
+export class Contact extends Entity implements IContact {
+ get email(): string | undefined { return this.asString(this.getMember('email')); }
+ set email(value: string | undefined) { this.setMember('email', value); }
+
+ readonly roles = new Strings(undefined, this, 'role');
+ /** @internal */
+ override *validate(): Iterable {
+ yield* super.validate();
+ }
+}
+
+export class Contacts extends EntityMap implements Dictionary {
+ constructor(node?: YAMLDictionary, parent?: Yaml, key?: string) {
+ super(Contact, node, parent, key);
+ }
+ /** @internal */
+ override *validate(): Iterable {
+ yield* super.validate();
+ if (this.exists()) {
+ for (const [key, contact] of this) {
+ yield* contact.validate();
+ }
+ }
+ }
+}
diff --git a/ce/ce/amf/demands.ts b/ce/ce/amf/demands.ts
new file mode 100644
index 0000000000..551faa296e
--- /dev/null
+++ b/ce/ce/amf/demands.ts
@@ -0,0 +1,352 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { stream } from 'fast-glob';
+import { lstat, Stats } from 'fs';
+import { delimiter, join, resolve } from 'path';
+import { isMap, isScalar } from 'yaml';
+import { Activation } from '../artifacts/activation';
+import { i } from '../i18n';
+import { ErrorKind } from '../interfaces/error-kind';
+import { AlternativeFulfillment } from '../interfaces/metadata/alternative-fulfillment';
+import { ValidationError } from '../interfaces/validation-error';
+import { parseQuery } from '../mediaquery/media-query';
+import { Session } from '../session';
+import { Evaluator } from '../util/evaluator';
+import { cmdlineToArray, execute } from '../util/exec-cmd';
+import { createSandbox } from '../util/safeEval';
+import { Entity } from '../yaml/Entity';
+import { EntityMap } from '../yaml/EntityMap';
+import { Strings } from '../yaml/strings';
+import { Primitive, Yaml, YAMLDictionary } from '../yaml/yaml-types';
+import { Installs } from './installer';
+import { Requires } from './Requires';
+import { Settings } from './settings';
+
+/** sandboxed eval function for evaluating expressions */
+const safeEval: (code: string, context?: any) => T = createSandbox();
+const hostFeatures = new Set(['x64', 'x86', 'arm', 'arm64', 'windows', 'linux', 'osx', 'freebsd']);
+
+const ignore = new Set(['info', 'contacts', 'error', 'message', 'warning', 'requires', 'see-also']);
+/**
+ * A map of mediaquery to DemandBlock
+ */
+export class Demands extends EntityMap {
+ constructor(node?: YAMLDictionary, parent?: Yaml, key?: string) {
+ super(DemandBlock, node, parent, key);
+ }
+
+ override get keys() {
+ return super.keys.filter(each => !ignore.has(each));
+ }
+
+ /** @internal */
+ override *validate(): Iterable {
+ yield* super.validate();
+
+ for (const [mediaQuery, demandBlock] of this) {
+ if (ignore.has(mediaQuery)) {
+ continue;
+ }
+ if (!isMap(demandBlock.node)) {
+ yield {
+ message: `Conditional demand '${mediaQuery}' is not an object`,
+ range: demandBlock.node!.range || [0, 0, 0],
+ category: ErrorKind.IncorrectType
+ };
+ continue;
+ }
+
+ const query = parseQuery(mediaQuery);
+ if (!query.isValid) {
+ yield { message: i`Error parsing conditional demand '${mediaQuery}'- ${query.error?.message}`, range: this.sourcePosition(mediaQuery)/* mediaQuery.range! */, rangeOffset: query.error, category: ErrorKind.ParseError };
+ continue;
+ }
+
+ yield* demandBlock.validate();
+ }
+ }
+}
+
+export class DemandBlock extends Entity {
+ #environment: Record = {};
+ #activation?: Activation;
+ #data?: Record;
+
+ setActivation(activation?: Activation) {
+ this.#activation = activation;
+ }
+
+ setData(data: Record) {
+ this.#data = data;
+ }
+
+ setEnvironment(env: Record) {
+ this.#environment = env;
+ }
+
+ protected get evaluationBlock() {
+ return new Evaluator(this.#data || {}, this.#environment, this.#activation?.output || {});
+ }
+ get error(): string | undefined { return this.usingAlternative ? this.unless.error : this.asString(this.getMember('error')); }
+ set error(value: string | undefined) { this.setMember('error', value); }
+
+ get warning(): string | undefined { return this.usingAlternative ? this.unless.warning : this.asString(this.getMember('warning')); }
+ set warning(value: string | undefined) { this.setMember('warning', value); }
+
+ get message(): string | undefined { return this.usingAlternative ? this.unless.warning : this.asString(this.getMember('message')); }
+ set message(value: string | undefined) { this.setMember('message', value); }
+
+ get seeAlso(): Requires {
+ return this.usingAlternative ? this.unless.seeAlso : this._seeAlso;
+ }
+
+ get requires(): Requires {
+ return this.usingAlternative ? this.unless.requires : this._requires;
+ }
+
+ get settings(): Settings {
+ return this.usingAlternative ? this.unless.settings : this._settings;
+ }
+
+ get install(): Installs {
+ return this.usingAlternative ? this.unless.install : this._install;
+ }
+
+ protected readonly _seeAlso = new Requires(undefined, this, 'seeAlso');
+ protected readonly _requires = new Requires(undefined, this, 'requires');
+ protected readonly _settings = new Settings(undefined, this, 'settings');
+ protected readonly _install = new Installs(undefined, this, 'install');
+
+ readonly unless!: Unless;
+
+ protected usingAlternative: boolean | undefined;
+
+ constructor(node?: YAMLDictionary, parent?: Yaml, key?: string) {
+ super(node, parent, key);
+ if (key !== 'unless') {
+ this.unless = new Unless(undefined, this, 'unless');
+ }
+ }
+
+ /**
+ * Async Initializer.
+ *
+ * checks the alternative demand resolution.
+ * when this runs, if the alternative is met, the rest of the demand is redirected to the alternative.
+ */
+ async init(session: Session): Promise {
+ this.#environment = session.environment;
+ if (this.usingAlternative === undefined && this.has('unless')) {
+ await this.unless.init(session);
+ this.usingAlternative = this.unless.usingAlternative;
+ }
+ return this;
+ }
+
+ /** @internal */
+ override *validate(): Iterable {
+ yield* super.validate();
+ if (this.exists()) {
+ yield* this.settings.validate();
+ yield* this.requires.validate();
+ yield* this.seeAlso.validate();
+ yield* this.install.validate();
+ }
+ }
+
+ override asString(value: any): string | undefined {
+ if (value === undefined) {
+ return value;
+ }
+ return this.evaluationBlock.evaluate(isScalar(value) ? value.value : value);
+ }
+
+ override asPrimitive(value: any): Primitive | undefined {
+ if (value === undefined) {
+ return value;
+ }
+ if (isScalar(value)) {
+ value = value.value;
+ }
+ switch (typeof value) {
+ case 'boolean':
+ case 'number':
+ return value;
+
+ case 'string': {
+ return this.evaluationBlock.evaluate(value);
+ }
+ }
+ return undefined;
+ }
+}
+
+/** Expands string variables in a string */
+function expandStrings(sandboxData: Record, value: string) {
+ let n = undefined;
+
+ // allow $PATH instead of ${PATH} -- simplifies YAML strings
+ value = value.replace(/\$([a-zA-Z0-9.]+)/g, '${$1}');
+
+ const parts = value.split(/(\${\S+?})/g).filter(each => each).map((each, i) => {
+ const v = each.replace(/^\${(.*)}$/, (m, match) => safeEval(match, sandboxData) ?? each);
+
+ if (v.indexOf(delimiter) !== -1) {
+ n = i;
+ }
+
+ return v;
+ });
+
+ if (n === undefined) {
+ return parts.join('');
+ }
+
+ const front = parts.slice(0, n).join('');
+ const back = parts.slice(n + 1).join('');
+
+ return parts[n].split(delimiter).filter(each => each).map(each => `${front}${each}${back}`).join(delimiter);
+}
+
+/** filters output and produces a sandbox context object */
+function filter(expression: string, content: string) {
+ const parsed = /^\/(.*)\/(\w*)$/.exec(expression);
+ const output = {
+ $content: content
+ };
+ if (parsed) {
+ const filtered = new RegExp(parsed[1], parsed[2]).exec(content);
+
+ if (filtered) {
+ for (const [i, v] of filtered.entries()) {
+ if (i === 0) {
+ continue;
+ }
+ output[`$${i}`] = v;
+ }
+ }
+ }
+ return output;
+}
+
+export class Unless extends DemandBlock implements AlternativeFulfillment {
+
+ readonly from = new Strings(undefined, this, 'from');
+ readonly where = new Strings(undefined, this, 'where');
+
+ get run(): string | undefined { return this.asString(this.getMember('run')); }
+ set run(value: string | undefined) { this.setMember('run', value); }
+
+ get select(): string | undefined { return this.asString(this.getMember('select')); }
+ set select(value: string | undefined) { this.setMember('select', value); }
+
+ get matches(): string | undefined { return this.asString(this.getMember('is')); }
+ set matches(value: string | undefined) { this.setMember('is', value); }
+
+ /** @internal */
+ override *validate(): Iterable {
+ // todo: what other validations do we need?
+ yield* super.validate();
+ if (this.has('unless')) {
+ yield {
+ message: '"unless" is not supported in an unless block',
+ range: this.sourcePosition('unless'),
+ category: ErrorKind.InvalidDefinition
+ };
+ }
+ }
+
+ override async init(session: Session): Promise {
+ this.setEnvironment(session.environment);
+ if (this.usingAlternative === undefined) {
+ this.usingAlternative = false;
+ if (this.from.length > 0 && this.where.length > 0) {
+ // we're doing some kind of check.
+ const locations = [...this.from].map(each => expandStrings(this.evaluationBlock, each).split(delimiter)).flat();
+ const binaries = [...this.where].map(each => expandStrings(this.evaluationBlock, each));
+
+ const search = locations.map(location => binaries.map(binary => join(location, binary).replace(/\\/g, '/'))).flat();
+
+ // when we find an adequate match, we stop looking
+ // to do so and not work hrd
+
+ const Break = {};
+ for await (const item of stream(search, {
+ concurrency: 1,
+ stats: false, fs: {
+ lstat: (path: string, callback: (error: NodeJS.ErrnoException | null, stats: Stats) => void) => {
+ // if we're done iterating, always return an error.
+ if (this.usingAlternative) {
+ return callback(Break, undefined);
+ }
+
+ return lstat(path, (error, stats) => {
+ // just return an error, as we don't want more results.
+ if (this.usingAlternative) {
+ // just return an error, as we don't want more results.
+ return callback(Break, undefined);
+ }
+
+ // symlink'd binaries on windows give us errors when it interrogates it too much.
+ if (stats && stats.mode === 41398) {
+ stats.mode = stats.mode & ~8192;
+ }
+ return callback(error, stats);
+ });
+ }
+ }
+ })) {
+ // we found something that looks promising.
+ let filtered = { $0: item };
+ this.setData(filtered);
+ if (this.run) {
+
+ const commandline = cmdlineToArray(this.run.replace('$0', item.toString()));
+ const result = await execute(resolve(commandline[0]), commandline.slice(1));
+ if (result.code !== 0) {
+ continue;
+ }
+
+ filtered = filter(this.select || '', result.log);
+ filtered.$0 = item;
+
+ // if we have a match expression, let's check it.
+ if (this.matches && !safeEval(this.matches, filtered)) {
+ continue; // not a match, move on
+ }
+
+ // it did match, or it's just presence check
+ this.usingAlternative = true;
+ // set the data output of the check
+ // this is used later to fill in the settings.
+ this.setData(filtered);
+ return this;
+ }
+ }
+ }
+ }
+ return this;
+ }
+
+ override get error(): string | undefined { return this.asString(this.getMember('error')); }
+ override get warning(): string | undefined { return this.asString(this.getMember('warning')); }
+ override get message(): string | undefined { return this.asString(this.getMember('message')); }
+
+ override get seeAlso(): Requires {
+ return this._seeAlso;
+ }
+
+ override get requires(): Requires {
+ return this._requires;
+ }
+
+ override get settings(): Settings {
+ return this._settings;
+ }
+
+ override get install(): Installs {
+ return this._install;
+ }
+
+}
diff --git a/ce/ce/amf/document-context.ts b/ce/ce/amf/document-context.ts
new file mode 100644
index 0000000000..29d2ed7da9
--- /dev/null
+++ b/ce/ce/amf/document-context.ts
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { LineCounter } from 'yaml';
+import { Session } from '../session';
+import { Uri } from '../util/uri';
+
+
+export interface DocumentContext {
+ session: Session;
+ filename: string;
+ file: Uri;
+ folder: Uri;
+ lineCounter: LineCounter;
+}
\ No newline at end of file
diff --git a/ce/ce/amf/global-settings.ts b/ce/ce/amf/global-settings.ts
new file mode 100644
index 0000000000..64cbd2d119
--- /dev/null
+++ b/ce/ce/amf/global-settings.ts
@@ -0,0 +1,9 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { ScalarMap } from '../yaml/ScalarMap';
+
+export class GlobalSettings extends ScalarMap {
+ // global settings is just a map at this point.
+}
+
diff --git a/ce/ce/amf/info.ts b/ce/ce/amf/info.ts
new file mode 100644
index 0000000000..011e5c85b8
--- /dev/null
+++ b/ce/ce/amf/info.ts
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { i } from '../i18n';
+import { ErrorKind } from '../interfaces/error-kind';
+import { Info as IInfo } from '../interfaces/metadata/info';
+import { ValidationError } from '../interfaces/validation-error';
+import { Entity } from '../yaml/Entity';
+import { Flags } from '../yaml/Flags';
+
+
+export class Info extends Entity implements IInfo {
+ get version(): string { return this.asString(this.getMember('version')) || ''; }
+ set version(value: string) { this.setMember('version', value); }
+
+ get id(): string { return this.asString(this.getMember('id')) || ''; }
+ set id(value: string) { this.setMember('id', value); }
+
+ get summary(): string | undefined { return this.asString(this.getMember('summary')); }
+ set summary(value: string | undefined) { this.setMember('summary', value); }
+
+ get priority(): number | undefined { return this.asNumber(this.getMember('priority')) || 0; }
+ set priority(value: number | undefined) { this.setMember('priority', value); }
+
+ get description(): string | undefined { return this.asString(this.getMember('description')); }
+ set description(value: string | undefined) { this.setMember('description', value); }
+
+ private flags = new Flags(undefined, this, 'options');
+
+ get dependencyOnly(): boolean { return this.flags.has('dependencyOnly'); }
+ set dependencyOnly(value: boolean) { this.flags.set('dependencyOnly', value); }
+
+ /** @internal */
+ override *validate(): Iterable {
+ yield* super.validate();
+
+ if (!this.has('id')) {
+ yield { message: i`Missing identity '${'info.id'}'`, range: this, category: ErrorKind.FieldMissing };
+ } else if (!this.is('id', 'string')) {
+ yield { message: i`info.id should be of type 'string', found '${this.kind('id')}'`, range: this.sourcePosition('id'), category: ErrorKind.IncorrectType };
+ }
+
+ if (!this.has('version')) {
+ yield { message: i`Missing version '${'info.version'}'`, range: this, category: ErrorKind.FieldMissing };
+ } else if (!this.is('version', 'string')) {
+ yield { message: i`info.version should be of type 'string', found '${this.kind('version')}'`, range: this.sourcePosition('version'), category: ErrorKind.IncorrectType };
+ }
+ if (this.is('summary', 'string') === false) {
+ yield { message: i`info.summary should be of type 'string', found '${this.kind('summary')}'`, range: this.sourcePosition('summary'), category: ErrorKind.IncorrectType };
+ }
+ if (this.is('description', 'string') === false) {
+ yield { message: i`info.description should be of type 'string', found '${this.kind('description')}'`, range: this.sourcePosition('description'), category: ErrorKind.IncorrectType };
+ }
+ if (this.is('options', 'sequence') === false) {
+ yield { message: i`info.options should be a sequence, found '${this.kind('options')}'`, range: this.sourcePosition('options'), category: ErrorKind.IncorrectType };
+ }
+ }
+}
diff --git a/ce/ce/amf/installer.ts b/ce/ce/amf/installer.ts
new file mode 100644
index 0000000000..8497d4799c
--- /dev/null
+++ b/ce/ce/amf/installer.ts
@@ -0,0 +1,194 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { isMap, isSeq } from 'yaml';
+import { GitInstaller } from '../interfaces/metadata/installers/git';
+import { Installer as IInstaller } from '../interfaces/metadata/installers/Installer';
+import { NupkgInstaller } from '../interfaces/metadata/installers/nupkg';
+import { UnTarInstaller } from '../interfaces/metadata/installers/tar';
+import { UnZipInstaller } from '../interfaces/metadata/installers/zip';
+import { Entity } from '../yaml/Entity';
+import { EntitySequence } from '../yaml/EntitySequence';
+import { Flags } from '../yaml/Flags';
+import { Strings } from '../yaml/strings';
+import { Node, Yaml, YAMLDictionary } from '../yaml/yaml-types';
+
+export class Installs extends EntitySequence {
+ constructor(node?: YAMLDictionary, parent?: Yaml, key?: string) {
+ super(Installer, node, parent, key);
+ }
+
+ override *[Symbol.iterator](): Iterator {
+ if (isMap(this.node)) {
+ yield this.createInstance(this.node);
+ }
+ if (isSeq(this.node)) {
+ for (const item of this.node.items) {
+ yield this.createInstance(item);
+ }
+ }
+ }
+
+ protected createInstance(node: Node): Installer {
+ if (isMap(node)) {
+ if (node.has('unzip')) {
+ return new UnzipNode(node, this);
+ }
+ if (node.has('nupkg')) {
+ return new NupkgNode(node, this);
+ }
+ if (node.has('untar')) {
+ return new UnTarNode(node, this);
+ }
+ if (node.has('git')) {
+ return new GitCloneNode(node, this);
+ }
+ }
+ throw new Error('Unsupported node type');
+ }
+}
+
+
+export class Installer extends Entity implements IInstaller {
+ get installerKind(): string {
+ throw new Error('abstract type, should not get here.');
+ }
+
+ get lang() {
+ return this.asString(this.getMember('lang'));
+ }
+
+ get nametag() {
+ return this.asString(this.getMember('nametag'));
+ }
+}
+
+abstract class FileInstallerNode extends Installer {
+ get sha256() {
+ return this.asString(this.getMember('sha256'));
+ }
+
+ set sha256(value: string | undefined) {
+ this.setMember('sha256', value);
+ }
+
+ get sha512() {
+ return this.asString(this.getMember('sha512'));
+ }
+
+ set sha512(value: string | undefined) {
+ this.setMember('sha512', value);
+ }
+
+ get strip() {
+ return this.asNumber(this.getMember('strip'));
+ }
+
+ set strip(value: number | undefined) {
+ this.setMember('1', value);
+ }
+
+ readonly transform = new Strings(undefined, this, 'transform');
+}
+class UnzipNode extends FileInstallerNode implements UnZipInstaller {
+ override get installerKind() { return 'unzip'; }
+
+ readonly location = new Strings(undefined, this, 'unzip');
+
+}
+class UnTarNode extends FileInstallerNode implements UnTarInstaller {
+ override get installerKind() { return 'untar'; }
+ location = new Strings(undefined, this, 'untar');
+
+}
+class NupkgNode extends Installer implements NupkgInstaller {
+ get location() {
+ return this.asString(this.getMember('nupkg'))!;
+ }
+
+ set location(value: string) {
+ this.setMember('nupkg', value);
+ }
+
+ override get installerKind() { return 'nupkg'; }
+
+ get strip() {
+ return this.asNumber(this.getMember('strip'));
+ }
+
+ set strip(value: number | undefined) {
+ this.setMember('1', value);
+ }
+
+ get sha256() {
+ return this.asString(this.getMember('sha256'));
+ }
+
+ set sha256(value: string | undefined) {
+ this.setMember('sha256', value);
+ }
+
+ get sha512() {
+ return this.asString(this.getMember('sha512'));
+ }
+
+ set sha512(value: string | undefined) {
+ this.setMember('sha512', value);
+ }
+
+ readonly transform = new Strings(undefined, this, 'transform');
+
+}
+class GitCloneNode extends Installer implements GitInstaller {
+ override get installerKind() { return 'git'; }
+
+ get location() {
+ return this.asString(this.getMember('git'))!;
+ }
+
+ set location(value: string) {
+ this.setMember('git', value);
+ }
+
+ get commit() {
+ return this.asString(this.getMember('commit'));
+ }
+
+ set commit(value: string | undefined) {
+ this.setMember('commit', value);
+ }
+
+ private flags = new Flags(undefined, this, 'options');
+
+ get full() {
+ return this.flags.has('full');
+ }
+
+ set full(value: boolean) {
+ this.flags.set('full', value);
+ }
+
+ get recurse() {
+ return this.flags.has('recurse');
+ }
+
+ set recurse(value: boolean) {
+ this.flags.set('recurse', value);
+ }
+
+ get subdirectory() {
+ return this.asString(this.getMember('subdirectory'));
+ }
+
+ set subdirectory(value: string | undefined) {
+ this.setMember('subdirectory', value);
+ }
+
+ get espidf() {
+ return this.flags.has('espidf');
+ }
+
+ set espidf(value: boolean) {
+ this.flags.set('espidf', value);
+ }
+}
diff --git a/ce/ce/amf/metadata-file.ts b/ce/ce/amf/metadata-file.ts
new file mode 100644
index 0000000000..e74c2337d9
--- /dev/null
+++ b/ce/ce/amf/metadata-file.ts
@@ -0,0 +1,216 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+
+import { extname } from 'path';
+import { Document, isMap, LineCounter, parseDocument, YAMLMap } from 'yaml';
+import { Activation } from '../artifacts/activation';
+import { Registry } from '../artifacts/registry';
+import { i } from '../i18n';
+import { ErrorKind } from '../interfaces/error-kind';
+import { Profile } from '../interfaces/metadata/metadata-format';
+import { ValidationError } from '../interfaces/validation-error';
+import { Session } from '../session';
+import { Uri } from '../util/uri';
+import { BaseMap } from '../yaml/BaseMap';
+import { toYAML } from '../yaml/yaml';
+import { Yaml, YAMLDictionary } from '../yaml/yaml-types';
+import { Contacts } from './contact';
+import { DemandBlock, Demands } from './demands';
+import { DocumentContext } from './document-context';
+import { GlobalSettings } from './global-settings';
+import { Info } from './info';
+import { Registries } from './registries';
+
+
+export class MetadataFile extends BaseMap implements Profile {
+ readonly context: DocumentContext;
+ session!: Session;
+
+ private constructor(protected document: Document.Parsed, public readonly filename: string, public lineCounter: LineCounter, public readonly registry: Registry | undefined) {
+ super(>document.contents);
+ this.context = {
+ filename,
+ lineCounter,
+ };
+ }
+
+ async init(session: Session): Promise {
+ this.context.session = session;
+ this.context.file = session.parseUri(this.context.filename);
+ this.context.folder = this.context.file.parent;
+ return this;
+ }
+
+ static async parseMetadata(uri: Uri, session: Session, registry?: Registry): Promise {
+ return MetadataFile.parseConfiguration(uri.path, await uri.readUTF8(), session, registry);
+ }
+
+ static async parseConfiguration(filename: string, content: string, session: Session, registry?: Registry): Promise {
+ const lc = new LineCounter();
+ if (!content || content === 'null') {
+ content = '{\n}';
+ }
+ const doc = parseDocument(content, { prettyErrors: false, lineCounter: lc, strict: true });
+ return new MetadataFile(doc, filename, lc, registry).init(session);
+ }
+
+ info = new Info(undefined, this, 'info');
+
+ contacts = new Contacts(undefined, this, 'contacts');
+ registries = new Registries(undefined, this, 'registries');
+ globalSettings = new GlobalSettings(undefined, this, 'global');
+
+ // rather than re-implement it, use encapsulatiob with a demand block
+ private demandBlock = new DemandBlock(this.node, undefined);
+
+ get error(): string | undefined { return this.demandBlock.error; }
+ set error(value: string | undefined) { this.demandBlock.error = value; }
+
+ get warning(): string | undefined { return this.demandBlock.warning; }
+ set warning(value: string | undefined) { this.demandBlock.warning = value; }
+
+ get message(): string | undefined { return this.demandBlock.message; }
+ set message(value: string | undefined) { this.demandBlock.message = value; }
+
+ get seeAlso() { return this.demandBlock.seeAlso; }
+ get requires() { return this.demandBlock.requires; }
+ get settings() { return this.demandBlock.settings; }
+ get install() { return this.demandBlock.install; }
+ get unless() { return this.demandBlock.unless; }
+
+ setActivation(activation: Activation): void {
+ this.demandBlock.setActivation(activation);
+ }
+
+ conditionalDemands = new Demands(undefined, this, 'demands');
+
+ get isFormatValid(): boolean {
+ return this.document.errors.length === 0;
+ }
+
+ get content() {
+ return toYAML(this.document.toString());
+ }
+
+ async save(uri: Uri = this.context.file): Promise {
+ // check the filename, and select the format.
+ let content = '';
+
+ switch (extname(uri.path).toLowerCase()) {
+ case '.yaml':
+ case '.yml':
+ // format as yaml
+ content = this.content;
+ break;
+
+ case '.json':
+ content = JSON.stringify(this.document.toJSON(), null, 2);
+ break;
+ default:
+ throw new Error(`Unsupported file type ${extname(uri.path)}`);
+ }
+ if (!content || content === 'null') {
+ content = '{\n}';
+ }
+ await uri.writeUTF8(content);
+ }
+ #errors!: Array;
+ get formatErrors(): Array {
+ const t = this;
+ return this.#errors || (this.#errors = this.document.errors.map(each => {
+ const message = each.message;
+ const line = each.linePos?.[0].line || 1;
+ const column = each.linePos?.[0].col || 1;
+ return t.formatMessage(each.name, message, line, column);
+ }));
+ }
+
+ /** @internal */ formatMessage(category: ErrorKind | string, message: string, line?: number, column?: number): string {
+ if (line !== undefined && column !== undefined) {
+ return `${this.filename}:${line}:${column} ${category}, ${message}`;
+ } else {
+ return `${this.filename}: ${category}, ${message}`;
+ }
+ }
+
+ get isValid(): boolean {
+ return this.validationErrors.length === 0;
+ }
+
+ #validationErrors!: Array;
+ get validationErrors(): Array {
+ if (this.#validationErrors) {
+ return this.#validationErrors;
+ }
+
+ const errs = new Set();
+ for (const { message, range, rangeOffset, category } of this.validate()) {
+ const r = Array.isArray(range) ? range : range?.sourcePosition();
+
+ const { line, column } = this.positionAt(r, rangeOffset);
+ errs.add(this.formatMessage(category, message, line, column));
+ }
+ this.#validationErrors = [...errs];
+ return this.#validationErrors;
+ }
+
+ private positionAt(range?: [number, number, number?], offset?: { line: number, column: number }) {
+ const { line, col } = this.lineCounter.linePos(range?.[0] || 0);
+
+ return offset ? {
+ // adds the offset values (which can come from the mediaquery parser) to the line & column. If MQ doesn't have a position, it's zero.
+ line: line + (offset.line - 1),
+ column: col + (offset.column - 1),
+ } :
+ {
+ line, column: col
+ };
+ }
+
+ /** @internal */
+ override *validate(): Iterable {
+ yield* super.validate();
+
+ // verify that we have info
+ if (!this.document.has('info')) {
+ yield { message: i`Missing section '${'info'}'`, range: this, category: ErrorKind.SectionNotFound };
+ } else {
+ yield* this.info.validate();
+ }
+
+
+ if (this.document.has('contacts')) {
+ for (const each of this.contacts.values) {
+ yield* each.validate();
+ }
+ }
+
+ const set = new Set();
+
+ for (const [mediaQuery, demandBlock] of this.conditionalDemands) {
+
+ if (set.has(mediaQuery)) {
+ yield { message: i`Duplicate keys detected in manifest: '${mediaQuery}'`, range: demandBlock, category: ErrorKind.DuplicateKey };
+ }
+ set.add(mediaQuery);
+
+ yield* demandBlock.validate();
+ }
+ yield* this.conditionalDemands.validate();
+ yield* this.install.validate();
+ yield* this.registries.validate();
+ yield* this.contacts.validate();
+ yield* this.settings.validate();
+ yield* this.globalSettings.validate();
+ yield* this.requires.validate();
+ yield* this.seeAlso.validate();
+ }
+
+ /** @internal */override assert(recreateIfDisposed = false, node = this.node): asserts this is Yaml & { node: YAMLDictionary } {
+ if (!isMap(this.node)) {
+ this.document = parseDocument('{\n}', { prettyErrors: false, lineCounter: this.context.lineCounter, strict: true });
+ this.node = >this.document.contents;
+ }
+ }
+}
diff --git a/ce/ce/amf/registries.ts b/ce/ce/amf/registries.ts
new file mode 100644
index 0000000000..0791504967
--- /dev/null
+++ b/ce/ce/amf/registries.ts
@@ -0,0 +1,216 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { isMap, isSeq, YAMLMap } from 'yaml';
+import { Dictionary } from '../interfaces/collections';
+import { ErrorKind } from '../interfaces/error-kind';
+import { RegistryDeclaration } from '../interfaces/metadata/metadata-format';
+import { Registry as IRegistry } from '../interfaces/metadata/registries/artifact-registry';
+import { ValidationError } from '../interfaces/validation-error';
+import { isFilePath, Uri } from '../util/uri';
+import { Entity } from '../yaml/Entity';
+import { Strings } from '../yaml/strings';
+import { Node, Yaml, YAMLDictionary, YAMLSequence } from '../yaml/yaml-types';
+
+export class Registries extends Yaml implements Dictionary, Iterable<[string, RegistryDeclaration]> {
+ *[Symbol.iterator](): Iterator<[string, RegistryDeclaration]> {
+ if (isMap(this.node)) {
+ for (const { key, value } of this.node.items) {
+ const v = this.createRegistry(value);
+ if (v) {
+ yield [key, v];
+ }
+ }
+ }
+ if (isSeq(this.node)) {
+ for (const item of this.node.items) {
+ if (isMap(item)) {
+ const name = this.asString(item.get('name'));
+ if (name) {
+ const v = this.createRegistry(item);
+ if (v) {
+ yield [name, v];
+ }
+ }
+ }
+ }
+ }
+ }
+
+ clear(): void {
+ this.dispose(true);
+ }
+
+ override createNode() {
+ return new YAMLSequence();
+ }
+
+ add(name: string, location?: Uri, kind?: string): RegistryDeclaration {
+ if (this.get(name)) {
+ throw new Error(`Registry ${name} already exists.`);
+ }
+
+ this.assert(true);
+ if (isMap(this.node)) {
+ throw new Error('Not Implemented as a map right now.');
+ }
+ if (isSeq(this.node)) {
+ const m = new YAMLMap();
+ this.node.add(m);
+ m.set('name', name);
+ m.set('location', location?.formatted);
+ m.set('kind', kind);
+ }
+ return this.get(name)!;
+ }
+ delete(key: string): boolean {
+ const n = this.node;
+ if (isMap(n)) {
+ const result = n.delete(key);
+ this.dispose();
+ return result;
+ }
+ if (isSeq(n)) {
+ let removed = false;
+ const items = n.items;
+ for (let i = items.length - 1; i >= 0; i--) {
+ const item = items[i];
+ if (isMap(item) && item.get('name') === key) {
+ removed ||= n.delete(i);
+ }
+ }
+ this.dispose();
+ return removed;
+ }
+ return false;
+ }
+ get(key: string): RegistryDeclaration | undefined {
+ const n = this.node;
+ if (isMap(n)) {
+ return this.createRegistry(n.get(key, true));
+ }
+ if (isSeq(n)) {
+ for (const item of n.items) {
+ if (isMap(item) && item.get('name') === key) {
+ return this.createRegistry(item);
+ }
+ }
+ }
+ return undefined;
+ }
+
+ has(key: string): boolean {
+ const n = this.node;
+ if (isMap(n)) {
+ return n.has(key);
+ }
+ if (isSeq(n)) {
+ for (const item of n.items) {
+ if (isMap(item) && item.get('name') === key) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ get length(): number {
+ if (isMap(this.node) || isSeq(this.node)) {
+ return this.node.items.length;
+ }
+ return 0;
+ }
+ get keys(): Array {
+ if (isMap(this.node)) {
+ return this.node.items.map(({ key }) => this.asString(key) || '');
+ }
+ if (isSeq(this.node)) {
+ const result = new Array();
+ for (const item of this.node.items) {
+ if (isMap(item)) {
+ const n = this.asString(item.get('name'));
+ if (n) {
+ result.push(n);
+ }
+ }
+ }
+ return result;
+ }
+ return [];
+ }
+
+ protected createRegistry(node: Node) {
+ if (isMap(node)) {
+ const k = this.asString(node.get('kind'));
+ const l = this.asString(node.get('location'));
+
+ // simplistic check to see if we're pointing to a file or a https:// url
+ if (k === 'artifact' && l) {
+ const ll = l?.toLowerCase();
+ if (ll.startsWith('https://')) {
+ return new RemoteRegistry(node, this);
+ }
+ if (isFilePath(l)) {
+ return new LocalRegistry(node, this);
+ }
+ }
+
+ }
+ return undefined;
+ }
+ /** @internal */
+ override *validate(): Iterable {
+ if (this.exists()) {
+ for (const [key, registry] of this) {
+ yield* registry.validate();
+ }
+ }
+ }
+}
+
+export class Registry extends Entity implements IRegistry {
+
+ get registryKind(): string | undefined { return this.asString(this.getMember('kind')); }
+ set registryKind(value: string | undefined) { this.setMember('kind', value); }
+
+ /** @internal */
+ override *validate(): Iterable {
+ //
+ if (this.registryKind === undefined) {
+ yield {
+ message: 'Registry missing \'kind\'',
+ range: this,
+ category: ErrorKind.FieldMissing,
+ };
+ }
+ }
+}
+
+class LocalRegistry extends Registry {
+ readonly location = new Strings(undefined, this, 'location');
+ /** @internal */
+ override *validate(): Iterable {
+ //
+ if (this.registryKind !== 'artifact') {
+ yield {
+ message: 'Registry \'kind\' is not correct for LocalRegistry ',
+ range: this,
+ category: ErrorKind.IncorrectType,
+ };
+ }
+ }
+}
+
+class RemoteRegistry extends Registry {
+ readonly location = new Strings(undefined, this, 'location');
+ override *validate(): Iterable {
+ //
+ if (this.registryKind !== 'artifact') {
+ yield {
+ message: 'Registry \'kind\' is not correct for LocalRegistry ',
+ range: this,
+ category: ErrorKind.IncorrectType,
+ };
+ }
+ }
+}
diff --git a/ce/ce/amf/settings.ts b/ce/ce/amf/settings.ts
new file mode 100644
index 0000000000..68ae300167
--- /dev/null
+++ b/ce/ce/amf/settings.ts
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+
+import { Settings as ISettings } from '../interfaces/metadata/Settings';
+import { ValidationError } from '../interfaces/validation-error';
+import { BaseMap } from '../yaml/BaseMap';
+import { ScalarMap } from '../yaml/ScalarMap';
+import { StringsMap } from '../yaml/strings';
+
+export class Settings extends BaseMap implements ISettings {
+ paths: StringsMap = new StringsMap(undefined, this, 'paths');
+ locations: ScalarMap = new ScalarMap(undefined, this, 'locations');
+ properties: StringsMap = new StringsMap(undefined, this, 'properties');
+ variables: StringsMap = new StringsMap(undefined, this, 'variables');
+ tools: ScalarMap = new ScalarMap(undefined, this, 'tools');
+ defines: ScalarMap = new ScalarMap(undefined, this, 'defines');
+
+ /** @internal */
+ override *validate(): Iterable {
+ // todo: what validations do we need?
+
+ }
+}
diff --git a/ce/ce/amf/version-reference.ts b/ce/ce/amf/version-reference.ts
new file mode 100644
index 0000000000..b9701e6356
--- /dev/null
+++ b/ce/ce/amf/version-reference.ts
@@ -0,0 +1,92 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { Range, SemVer } from 'semver';
+import { VersionReference as IVersionReference } from '../interfaces/metadata/version-reference';
+import { Yaml, YAMLScalar } from '../yaml/yaml-types';
+
+
+// nuget-semver parser doesn't have a ts typings package
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const parseRange: any = require('@snyk/nuget-semver/lib/range-parser');
+
+export class VersionReference extends Yaml implements IVersionReference {
+ get raw(): string | undefined {
+ return this.node?.value || undefined;
+ }
+
+ set raw(value: string | undefined) {
+ if (value === undefined) {
+ this.dispose(true);
+ } else {
+ this.node = new YAMLScalar(value);
+ }
+ }
+
+ static override create(): YAMLScalar {
+ return new YAMLScalar('');
+ }
+
+ private split(): [Range, SemVer | undefined] {
+
+ const v = this.raw;
+ if (v) {
+
+ const [, a, b] = /(.+)\s+([\d\\.]+)/.exec(v) || [];
+
+ if (/\[|\]|\(|\)/.exec(v)) {
+ // looks like a nuget version range.
+ try {
+ const range = parseRange(a || v);
+ let str = '';
+ if (range._components[0].minOperator) {
+ str = `${range._components[0].minOperator} ${range._components[0].minOperand}`;
+ }
+ if (range._components[0].maxOperator) {
+ str = `${str} ${range._components[0].maxOperator} ${range._components[0].maxOperand}`;
+ }
+ const newRange = new Range(str);
+ newRange.raw = a || v;
+
+ if (b) {
+ const ver = new SemVer(b, true);
+ return [newRange, ver];
+ }
+
+ return [newRange, undefined];
+
+ } catch (E) {
+ // ignore and fall thru
+ }
+ }
+
+ if (a) {
+ // we have at least a range going on here.
+ try {
+ const range = new Range(a, true);
+ const ver = new SemVer(b, true);
+ return [range, ver];
+ } catch (E) {
+ // ignore and fall thru
+ }
+ }
+ // the range or version didn't resolve correctly.
+ // must be a range alone.
+ return [new Range(v, true), undefined];
+ }
+ return [new Range('*', true), undefined];
+ }
+ get range() {
+ return this.split()[0];
+ }
+ set range(ver: Range) {
+ this.raw = `${ver.raw} ${this.resolved?.raw || ''}`.trim();
+ }
+
+ get resolved() {
+ return this.split()[1];
+ }
+ set resolved(ver: SemVer | undefined) {
+ this.raw = `${this.range.raw} ${ver?.raw || ''}`.trim();
+ }
+}
\ No newline at end of file
diff --git a/ce/ce/archivers/ZipUnpacker.ts b/ce/ce/archivers/ZipUnpacker.ts
new file mode 100644
index 0000000000..898357b43a
--- /dev/null
+++ b/ce/ce/archivers/ZipUnpacker.ts
@@ -0,0 +1,71 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { ProgressTrackingStream } from '../fs/streams';
+import { InstallEvents } from '../interfaces/events';
+import { Session } from '../session';
+import { PercentageScaler } from '../util/percentage-scaler';
+import { Queue } from '../util/promise';
+import { Uri } from '../util/uri';
+import { UnpackOptions } from './options';
+import { pipeline, Unpacker } from './unpacker';
+import { ZipEntry, ZipFile } from './unzip';
+
+export class ZipUnpacker extends Unpacker {
+ constructor(private readonly session: Session) {
+ super();
+ }
+
+ async unpackFile(file: ZipEntry, archiveUri: Uri, outputUri: Uri, options: UnpackOptions) {
+ const extractPath = Unpacker.implementOutputOptions(file.name, options);
+ if (extractPath) {
+ const destination = outputUri.join(extractPath);
+ const fileEntry = {
+ archiveUri,
+ destination,
+ path: file.name,
+ extractPath
+ };
+
+ this.fileProgress(fileEntry, 0);
+ this.session.channels.debug(`unpacking ZIP file ${archiveUri}/${file.name} => ${destination}`);
+ await destination.parent.createDirectory();
+ const readStream = await file.read();
+ const mode = ((file.attr >> 16) & 0xfff);
+
+ const writeStream = await destination.writeStream({ mtime: file.time, mode: mode ? mode : undefined });
+ const progressStream = new ProgressTrackingStream(0, file.size);
+ progressStream.on('progress', (filePercentage) => this.fileProgress(fileEntry, filePercentage));
+ await pipeline(readStream, progressStream, writeStream);
+ this.fileProgress(fileEntry, 100);
+ this.unpacked(fileEntry);
+ }
+ }
+
+ async unpack(archiveUri: Uri, outputUri: Uri, events: Partial, options: UnpackOptions): Promise {
+ this.subscribe(events);
+ try {
+ this.session.channels.debug(`unpacking ZIP ${archiveUri} => ${outputUri}`);
+
+ const openedFile = await archiveUri.openFile();
+ const zipFile = await ZipFile.read(openedFile);
+
+ const archiveProgress = new PercentageScaler(0, zipFile.files.size);
+ this.progress(0);
+ const q = new Queue();
+ let count = 0;
+ for (const file of zipFile.files.values()) {
+
+ void q.enqueue(async () => {
+ await this.unpackFile(file, archiveUri, outputUri, options);
+ this.progress(archiveProgress.scalePosition(count++));
+ });
+ }
+ await q.done;
+ await zipFile.close();
+ this.progress(100);
+ } finally {
+ this.unsubscribe(events);
+ }
+ }
+}
diff --git a/ce/ce/archivers/git.ts b/ce/ce/archivers/git.ts
new file mode 100644
index 0000000000..8c07df9af0
--- /dev/null
+++ b/ce/ce/archivers/git.ts
@@ -0,0 +1,147 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { InstallEvents } from '../interfaces/events';
+import { Session } from '../session';
+import { Credentials } from '../util/credentials';
+import { execute } from '../util/exec-cmd';
+import { isFilePath, Uri } from '../util/uri';
+
+export interface CloneOptions {
+ force?: boolean;
+ credentials?: Credentials;
+}
+
+/** @internal */
+export class Git {
+ #session: Session;
+ #toolPath: string;
+ #targetFolder: string;
+ #environment: NodeJS.ProcessEnv;
+
+ constructor(session: Session, toolPath: string, environment: NodeJS.ProcessEnv, targetFolder: Uri) {
+ this.#session = session;
+ this.#toolPath = toolPath;
+ this.#targetFolder = targetFolder.fsPath;
+ this.#environment = environment;
+ }
+
+ /**
+ * Method that clones a git repo into a desired location and with various options.
+ * @param repo The Uri of the remote repository that is desired to be cloned.
+ * @param events The events that may need to be updated in order to track progress.
+ * @param options The options that will modify how the clone will be called.
+ * @returns Boolean representing whether the execution was completed without error, this is not necessarily
+ * a gaurantee that the clone did what we expected.
+ */
+ async clone(repo: Uri, events: Partial, options: { recursive?: boolean, depth?: number } = {}) {
+ const remote = await isFilePath(repo) ? repo.fsPath : repo.toString();
+
+ const result = await execute(this.#toolPath, [
+ 'clone',
+ remote,
+ this.#targetFolder,
+ options.recursive ? '--recursive' : '',
+ options.depth ? `--depth=${options.depth}` : '',
+ '--progress'
+ ], {
+ env: this.#environment,
+ onStdErrData: (chunk) => {
+ // generate progress events
+ // this.#session.channels.debug(chunk.toString());
+ const regex = /\s([0-9]*?)%/;
+ chunk.toString().split(/^/gim).map((x: string) => x.trim()).filter((each: any) => each).forEach((line: string) => {
+ const match_array = line.match(regex);
+ if (match_array !== null) {
+ events.heartbeat?.(line.trim());
+ }
+ });
+ }
+ });
+
+ if (result.code) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Fetches a 'tag', this could theoretically be a commit, a tag, or a branch.
+ * @param remoteName Remote name to fetch from. Typically will be 'origin'.
+ * @param events Events that may be called in order to present progress.
+ * @param options Options to modify how fetch is called.
+ * @returns Boolean representing whether the execution was completed without error, this is not necessarily
+ * a gaurantee that the fetch did what we expected.
+ */
+ async fetch(remoteName: string, events: Partial, options: { commit?: string, recursive?: boolean, depth?: number } = {}) {
+ const result = await execute(this.#toolPath, [
+ '-C',
+ this.#targetFolder,
+ 'fetch',
+ remoteName,
+ options.commit ? options.commit : '',
+ options.recursive ? '--recurse-submodules' : '',
+ options.depth ? `--depth=${options.depth}` : ''
+ ], {
+ env: this.#environment
+ });
+
+ if (result.code) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks out a specific commit. If no commit is given, the default behavior of a checkout will be
+ * used. (Checking out the current branch)
+ * @param events Events to possibly track progress.
+ * @param options Passing along a commit or branch to checkout, optionally.
+ * @returns Boolean representing whether the execution was completed without error, this is not necessarily
+ * a gaurantee that the checkout did what we expected.
+ */
+ async checkout(events: Partial, options: { commit?: string } = {}) {
+ const result = await execute(this.#toolPath, [
+ '-C',
+ this.#targetFolder,
+ 'checkout',
+ options.commit ? options.commit : ''
+ ], {
+ env: this.#environment
+ });
+
+ if (result.code) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Performs a reset on the git repo.
+ * @param events Events to possibly track progress.
+ * @param options Options to control how the reset is called.
+ * @returns Boolean representing whether the execution was completed without error, this is not necessarily
+ * a gaurantee that the reset did what we expected.
+ */
+ async reset(events: Partial, options: { commit?: string, recurse?: boolean, hard?: boolean } = {}) {
+ const result = await execute(this.#toolPath, [
+ '-C',
+ this.#targetFolder,
+ 'reset',
+ options.commit ? options.commit : '',
+ options.recurse ? '--recurse-submodules' : '',
+ options.hard ? '--hard' : ''
+ ], {
+ env: this.#environment
+ });
+
+ if (result.code) {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/ce/ce/archivers/options.ts b/ce/ce/archivers/options.ts
new file mode 100644
index 0000000000..337d89bbb2
--- /dev/null
+++ b/ce/ce/archivers/options.ts
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+/** Unpacker output options */
+
+export interface UnpackOptions {
+ /**
+ * Strip # directories from the path
+ *
+ * Typically used to remove excessive nested folders off the front of the paths in an archive.
+ */
+ strip?: number;
+
+ /**
+ * A regular expression to transform filenames during unpack. If the resulting file name is empty, it is not emitted.
+ */
+ transform?: Array;
+}
diff --git a/ce/ce/archivers/tar.ts b/ce/ce/archivers/tar.ts
new file mode 100644
index 0000000000..ed8e22e1d7
--- /dev/null
+++ b/ce/ce/archivers/tar.ts
@@ -0,0 +1,141 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { fail } from 'assert';
+import { pipeline as origPipeline, Readable, Transform } from 'stream';
+import { extract as tarExtract, Headers } from 'tar-stream';
+import { promisify } from 'util';
+import { createGunzip } from 'zlib';
+import { ProgressTrackingStream } from '../fs/streams';
+import { UnifiedFileSystem } from '../fs/unified-filesystem';
+import { i } from '../i18n';
+import { InstallEvents } from '../interfaces/events';
+import { Session } from '../session';
+import { Uri } from '../util/uri';
+import { UnpackOptions } from './options';
+import { Unpacker } from './unpacker';
+
+export const pipeline = promisify(origPipeline);
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const bz2 = require('unbzip2-stream');
+
+abstract class BasicTarUnpacker extends Unpacker {
+ constructor(protected readonly session: Session) {
+ super();
+ }
+
+ async maybeUnpackEntry(archiveUri: Uri, outputUri: Uri, events: Partial, options: UnpackOptions, header: Headers, stream: Readable): Promise {
+ const streamPromise = new Promise((accept, reject) => {
+ stream.on('end', accept);
+ stream.on('error', reject);
+ });
+
+ try {
+ const extractPath = Unpacker.implementOutputOptions(header.name, options);
+ let destination: Uri | undefined = undefined;
+ if (extractPath) {
+ destination = outputUri.join(extractPath);
+ }
+
+ if (destination) {
+ switch (header?.type) {
+ case 'symlink': {
+ const linkTargetUri = destination?.parent.join(header.linkname!) || fail('');
+ await destination.parent.createDirectory();
+ await (this.session.fileSystem).filesystem(linkTargetUri).createSymlink(linkTargetUri, destination!);
+ }
+ return;
+
+ case 'link': {
+ // this should be a 'hard-link' -- but I'm not sure if we can make hardlinks on windows. todo: find out
+ const linkTargetUri = outputUri.join(Unpacker.implementOutputOptions(header.linkname!, options)!);
+ // quick hack
+ await destination.parent.createDirectory();
+ await (this.session.fileSystem).filesystem(linkTargetUri).createSymlink(linkTargetUri, destination!);
+ }
+ return;
+
+ case 'directory':
+ this.session.channels.debug(`in ${archiveUri.fsPath} skipping directory ${header.name}`);
+ return;
+
+ case 'file':
+ // files handle below
+ break;
+
+ default:
+ this.session.channels.warning(i`in ${archiveUri.fsPath} skipping ${header.name} because it is a ${header?.type || ''}`);
+ return;
+ }
+
+ const fileEntry = {
+ archiveUri: archiveUri,
+ destination: destination,
+ path: header.name,
+ extractPath: extractPath
+ };
+
+ this.session.channels.debug(`unpacking TAR ${archiveUri}/${header.name} => ${destination}`);
+ this.fileProgress(fileEntry, 0);
+
+ if (header.size) {
+ const parentDirectory = destination.parent;
+ await parentDirectory.createDirectory();
+ const fileProgress = new ProgressTrackingStream(0, header.size);
+ fileProgress.on('progress', (filePercentage) => this.fileProgress(fileEntry, filePercentage));
+ fileProgress.on('progress', (filePercentage) => events?.fileProgress?.(fileEntry, filePercentage));
+ const writeStream = await destination.writeStream({ mtime: header.mtime, mode: header.mode });
+ await pipeline(stream, fileProgress, writeStream);
+ }
+
+ this.fileProgress(fileEntry, 100);
+ this.unpacked(fileEntry);
+ }
+
+ } finally {
+ stream.resume();
+ await streamPromise;
+ }
+ }
+
+ protected async unpackTar(archiveUri: Uri, outputUri: Uri, events: Partial, options: UnpackOptions, decompressor?: Transform): Promise {
+ this.subscribe(events);
+ const archiveSize = await archiveUri.size();
+ const archiveFileStream = await archiveUri.readStream(0, archiveSize);
+ const archiveProgress = new ProgressTrackingStream(0, archiveSize);
+ const tarExtractor = tarExtract();
+
+ tarExtractor.on('entry', (header, stream, next) =>
+ this.maybeUnpackEntry(archiveUri, outputUri, events, options, header, stream).then(() => {
+ this.progress(archiveProgress.currentPercentage);
+ next();
+ }).catch(err => (next)(err)));
+
+ if (decompressor) {
+ await pipeline(archiveFileStream, archiveProgress, decompressor, tarExtractor);
+ } else {
+ await pipeline(archiveFileStream, archiveProgress, tarExtractor);
+ }
+ }
+}
+
+export class TarUnpacker extends BasicTarUnpacker {
+ unpack(archiveUri: Uri, outputUri: Uri, events: Partial, options: UnpackOptions): Promise {
+ this.session.channels.debug(`unpacking TAR ${archiveUri} => ${outputUri}`);
+ return this.unpackTar(archiveUri, outputUri, events, options);
+ }
+}
+
+export class TarGzUnpacker extends BasicTarUnpacker {
+ unpack(archiveUri: Uri, outputUri: Uri, events: Partial, options: UnpackOptions): Promise {
+ this.session.channels.debug(`unpacking TAR.GZ ${archiveUri} => ${outputUri}`);
+ return this.unpackTar(archiveUri, outputUri, events, options, createGunzip());
+ }
+}
+
+export class TarBzUnpacker extends BasicTarUnpacker {
+ unpack(archiveUri: Uri, outputUri: Uri, events: Partial, options: UnpackOptions): Promise {
+ this.session.channels.debug(`unpacking TAR.BZ2 ${archiveUri} => ${outputUri}`);
+ return this.unpackTar(archiveUri, outputUri, events, options, bz2());
+ }
+}
diff --git a/ce/ce/archivers/unpacker.ts b/ce/ce/archivers/unpacker.ts
new file mode 100644
index 0000000000..f3b4584878
--- /dev/null
+++ b/ce/ce/archivers/unpacker.ts
@@ -0,0 +1,97 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { sed } from 'sed-lite';
+import { pipeline as origPipeline } from 'stream';
+import { promisify } from 'util';
+import { InstallEvents, UnpackEvents } from '../interfaces/events';
+import { ExtendedEmitter } from '../util/events';
+import { Uri } from '../util/uri';
+import { UnpackOptions } from './options';
+
+export const pipeline = promisify(origPipeline);
+
+export interface FileEntry {
+ archiveUri: Uri;
+ destination: Uri | undefined;
+ path: string;
+ extractPath: string | undefined;
+}
+
+/** Unpacker base class definition */
+export abstract class Unpacker extends ExtendedEmitter {
+ /* Event Emitters */
+
+ /** EventEmitter: progress, at least once per file */
+ protected progress(archivePercentage: number): void {
+ this.emit('progress', archivePercentage);
+ }
+ protected fileProgress(entry: Readonly, filePercentage: number): void {
+ this.emit('fileProgress', entry, filePercentage);
+ }
+ /** EventEmitter: unpacked, emitted per file (not per archive) */
+ protected unpacked(entry: Readonly) {
+ this.emit('unpacked', entry);
+ }
+
+ abstract unpack(archiveUri: Uri, outputUri: Uri, events: Partial, options: UnpackOptions): Promise;
+
+ /**
+ * Returns a new path string such that the path has prefixCount path elements removed, and directory
+ * separators normalized to a single forward slash.
+ * If prefixCount is greater than or equal to the number of path elements in the path, undefined is returned.
+ */
+ public static stripPath(path: string, prefixCount: number): string | undefined {
+ const elements = path.split(/[\\/]+/);
+ const hasLeadingSlash = elements.length !== 0 && elements[0].length === 0;
+ const hasTrailingSlash = elements.length !== 0 && elements[elements.length - 1].length === 0;
+ let countForUndefined = prefixCount;
+ if (hasLeadingSlash) {
+ ++countForUndefined;
+ }
+
+ if (hasTrailingSlash) {
+ ++countForUndefined;
+ }
+
+ if (elements.length <= countForUndefined) {
+ return undefined;
+ }
+
+ if (hasLeadingSlash) {
+ return '/' + elements.splice(prefixCount + 1).join('/');
+ }
+
+ return elements.splice(prefixCount).join('/');
+ }
+
+ /**
+ * Apply OutputOptions to a path before extraction.
+ * @param entry The initial path to a file to unpack.
+ * @param options Options to apply to that file name.
+ * @returns If the file is to be emitted, the path to use; otherwise, undefined.
+ */
+ protected static implementOutputOptions(path: string, options: UnpackOptions): string | undefined {
+ if (options.strip) {
+ const maybeStripped = Unpacker.stripPath(path, options.strip);
+ if (maybeStripped) {
+ path = maybeStripped;
+ } else {
+ return undefined;
+ }
+ }
+
+ if (options.transform) {
+ for (const transformExpr of options.transform) {
+ if (!path) {
+ break;
+ }
+
+ const sedTransformExpr = sed(transformExpr);
+ path = sedTransformExpr(path);
+ }
+ }
+
+ return path;
+ }
+}
diff --git a/ce/ce/archivers/unzip.ts b/ce/ce/archivers/unzip.ts
new file mode 100644
index 0000000000..b22270fa71
--- /dev/null
+++ b/ce/ce/archivers/unzip.ts
@@ -0,0 +1,797 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+/**
+ * @license node-stream-zip | (c) 2020 Antelle | https://github.com/antelle/node-stream-zip/blob/master/LICENSE
+ * Portions copyright https://github.com/cthackers/adm-zip | https://raw.githubusercontent.com/cthackers/adm-zip/master/LICENSE
+ */
+
+import { Readable, Transform, TransformCallback } from 'stream';
+import { constants, createInflateRaw } from 'zlib';
+import { ReadHandle } from '../fs/filesystem';
+
+interface ZipParseState {
+ window: FileWindowBuffer;
+ totalReadLength: number;
+ minPos: number;
+ lastPos: any;
+ chunkSize: any;
+ firstByte: number;
+ sig: number;
+ lastBufferPosition?: number;
+ pos: number;
+ entry: ZipEntry | null;
+ entriesLeft: number;
+ move: boolean;
+}
+
+export class ZipFile {
+ private centralDirectory!: CentralDirectoryHeader;
+ private chunkSize = 0;
+ private state!: ZipParseState;
+ private fileSize = 0;
+
+ /** file entries in the zip file */
+ public files = new Map();
+
+ /** folder entries in the zip file */
+ public folders = new Map();
+
+ /**
+ * archive comment
+ */
+ public comment?: string;
+
+ static async read(readHandle: ReadHandle) {
+ const result = new ZipFile(readHandle);
+ await result.readCentralDirectory();
+ return result;
+ }
+
+ close() {
+ return this.readHandle.close();
+ }
+
+ private constructor(private readHandle: ReadHandle) {
+ }
+
+ private async readUntilFound(): Promise {
+ let pos = this.state.lastPos;
+ let bufferPosition = pos - this.state.window.position;
+ const buffer = this.state.window.buffer;
+ const minPos = this.state.minPos;
+ while (--pos >= minPos && --bufferPosition >= 0) {
+ if (buffer.length - bufferPosition >= 4 && buffer[bufferPosition] === this.state.firstByte) {
+ // quick check first signature byte
+ if (buffer.readUInt32LE(bufferPosition) === this.state.sig) {
+ this.state.lastBufferPosition = bufferPosition;
+ return;
+ }
+ }
+ }
+ if (pos === minPos) {
+ throw new Error('Bad archive');
+ }
+
+ this.state.lastPos = pos + 1;
+ this.state.chunkSize *= 2;
+ if (pos <= minPos) {
+ throw new Error('Bad archive');
+ }
+
+ const expandLength = Math.min(this.state.chunkSize, pos - minPos);
+ await this.state.window.expandLeft(expandLength);
+ return this.readUntilFound();
+ }
+
+ async readCentralDirectory() {
+ this.fileSize = await this.readHandle.size();
+ this.chunkSize = this.chunkSize || Math.round(this.fileSize / 1000);
+ this.chunkSize = Math.max(
+ Math.min(this.chunkSize, Math.min(128 * 1024, this.fileSize)),
+ Math.min(1024, this.fileSize)
+ );
+
+ const totalReadLength = Math.min(consts.ENDHDR + consts.MAXFILECOMMENT, this.fileSize);
+ this.state = {
+ window: new FileWindowBuffer(this.readHandle),
+ totalReadLength,
+ minPos: this.fileSize - totalReadLength,
+ lastPos: this.fileSize,
+ chunkSize: Math.min(1024, this.chunkSize),
+ firstByte: consts.ENDSIGFIRST,
+ sig: consts.ENDSIG,
+ };
+ await this.state.window.read(this.fileSize - this.state.chunkSize, this.state.chunkSize);
+ await this.readUntilFound();
+
+ const buffer = this.state.window.buffer;
+ const pos = this.state.lastBufferPosition || 0;
+
+ this.centralDirectory = new CentralDirectoryHeader(buffer.slice(pos, pos + consts.ENDHDR));
+
+ this.centralDirectory.headerOffset = this.state.window.position + pos;
+ this.comment = this.centralDirectory.commentLength ? buffer.slice(pos + consts.ENDHDR, pos + consts.ENDHDR + this.centralDirectory.commentLength).toString() : undefined;
+
+ if ((this.centralDirectory.volumeEntries === consts.EF_ZIP64_OR_16 && this.centralDirectory.totalEntries === consts.EF_ZIP64_OR_16) || this.centralDirectory.size === consts.EF_ZIP64_OR_32 || this.centralDirectory.offset === consts.EF_ZIP64_OR_32) {
+ return this.readZip64CentralDirectoryLocator();
+ } else {
+ this.state = {};
+ return this.readEntries();
+ }
+ }
+
+ private async readZip64CentralDirectoryLocator() {
+ const length = consts.ENDL64HDR;
+ if (this.state.lastBufferPosition! > length) {
+ this.state.lastBufferPosition! -= length;
+ } else {
+ this.state = {
+ window: this.state.window,
+ totalReadLength: length,
+ minPos: this.state.window.position - length,
+ lastPos: this.state.window.position,
+ chunkSize: this.state.chunkSize,
+ firstByte: consts.ENDL64SIGFIRST,
+ sig: consts.ENDL64SIG,
+ };
+ await this.state.window.read(this.state.lastPos - this.state.chunkSize, this.state.chunkSize);
+ await this.readUntilFound();
+ }
+
+ let buffer = this.state.window.buffer;
+ const locHeader = new CentralDirectoryLoc64Header(
+ buffer.slice(this.state.lastBufferPosition, this.state.lastBufferPosition! + consts.ENDL64HDR)
+ );
+ const readLength = this.fileSize - locHeader.headerOffset;
+ this.state = {
+ window: this.state.window,
+ totalReadLength: readLength,
+ minPos: locHeader.headerOffset,
+ lastPos: this.state.lastPos,
+ chunkSize: this.state.chunkSize,
+ firstByte: consts.END64SIGFIRST,
+ sig: consts.END64SIG,
+ };
+ await this.state.window.read(this.fileSize - this.state.chunkSize, this.state.chunkSize);
+ await this.readUntilFound();
+
+ buffer = this.state.window.buffer;
+ const zip64cd = new CentralDirectoryZip64Header(buffer.slice(this.state.lastBufferPosition, this.state.lastBufferPosition! + consts.END64HDR));
+ this.centralDirectory.volumeEntries = zip64cd.volumeEntries;
+ this.centralDirectory.totalEntries = zip64cd.totalEntries;
+ this.centralDirectory.size = zip64cd.size;
+ this.centralDirectory.offset = zip64cd.offset;
+
+ this.state = {};
+ return this.readEntries();
+ }
+
+ private async readEntries() {
+ this.state = {
+ window: new FileWindowBuffer(this.readHandle),
+ pos: this.centralDirectory.offset,
+ chunkSize: this.chunkSize,
+ entriesLeft: this.centralDirectory.volumeEntries,
+ };
+
+ await this.state.window.read(this.state.pos, Math.min(this.chunkSize, this.fileSize - this.state.pos));
+
+ while (this.state.entriesLeft) {
+ let bufferPos = this.state.pos - this.state.window.position;
+ let entry = this.state.entry;
+ const buffer = this.state.window.buffer;
+ const bufferLength = buffer.length;
+
+ if (!entry) {
+ entry = new ZipEntry(this, buffer, bufferPos);
+ entry.headerOffset = this.state.window.position + bufferPos;
+ this.state.entry = entry;
+ this.state.pos += consts.CENHDR;
+ bufferPos += consts.CENHDR;
+ }
+ const entryHeaderSize = entry.fnameLen + entry.extraLen + entry.comLen;
+ const advanceBytes = entryHeaderSize + (this.state.entriesLeft > 1 ? consts.CENHDR : 0);
+
+ // if there isn't enough bytes read, read 'em.
+ if (bufferLength - bufferPos < advanceBytes) {
+ await this.state.window.moveRight(bufferPos);
+ continue;
+ }
+
+ entry.processFilename(buffer, bufferPos);
+ entry.validateName();
+ if (entry.isDirectory) {
+ this.folders.set(entry.name, entry);
+ } else {
+ this.files.set(entry.name, entry);
+ }
+
+ this.state.entry = entry = null;
+ this.state.entriesLeft--;
+ this.state.pos += entryHeaderSize;
+ bufferPos += entryHeaderSize;
+ }
+ }
+
+ private dataOffset(entry: ZipEntry) {
+ return entry.offset + consts.LOCHDR + entry.fnameLen + entry.extraLen;
+ }
+
+ /** @internal */
+ async openEntry(entry: ZipEntry) {
+ // is this a file?
+ if (!entry.isFile) {
+ throw new Error(`Entry ${entry} not is not a file`);
+ }
+
+ // let's check to see if it's encrypted.
+ const buffer = Buffer.alloc(consts.LOCHDR);
+ await this.readHandle.readComplete(buffer, 0, buffer.length, entry.offset);
+
+ entry.parseDataHeader(buffer);
+ if (entry.encrypted) {
+ throw new Error('Entry encrypted');
+ }
+
+ const offset = this.dataOffset((entry));
+ let entryStream = this.readHandle.readStream(offset, offset + entry.compressedSize - 1);
+
+ if (entry.method === consts.STORED) {
+ // nothing to do
+ } else if (entry.method === consts.DEFLATED) {
+ entryStream = entryStream.pipe(createInflateRaw({ flush: constants.Z_SYNC_FLUSH }));
+ } else {
+ throw new Error('Unknown compression method: ' + entry.method);
+ }
+
+ // should check CRC?
+ if ((entry.flags & 0x8) === 0x8) {
+ entryStream = entryStream.pipe(createVerifier(entry.crc, entry.size));
+ }
+
+ return entryStream;
+ }
+}
+
+const consts = {
+ /* The local file header */
+ LOCHDR: 30, // LOC header size
+ LOCSIG: 0x04034b50, // "PK\003\004"
+ LOCVER: 4, // version needed to extract
+ LOCFLG: 6, // general purpose bit flag
+ LOCHOW: 8, // compression method
+ LOCTIM: 10, // modification time (2 bytes time, 2 bytes date)
+ LOCCRC: 14, // uncompressed file crc-32 value
+ LOCSIZ: 18, // compressed size
+ LOCLEN: 22, // uncompressed size
+ LOCNAM: 26, // filename length
+ LOCEXT: 28, // extra field length
+
+ /* The Data descriptor */
+ EXTSIG: 0x08074b50, // "PK\007\008"
+ EXTHDR: 16, // EXT header size
+ EXTCRC: 4, // uncompressed file crc-32 value
+ EXTSIZ: 8, // compressed size
+ EXTLEN: 12, // uncompressed size
+
+ /* The central directory file header */
+ CENHDR: 46, // CEN header size
+ CENSIG: 0x02014b50, // "PK\001\002"
+ CENVEM: 4, // version made by
+ CENVER: 6, // version needed to extract
+ CENFLG: 8, // encrypt, decrypt flags
+ CENHOW: 10, // compression method
+ CENTIM: 12, // modification time (2 bytes time, 2 bytes date)
+ CENCRC: 16, // uncompressed file crc-32 value
+ CENSIZ: 20, // compressed size
+ CENLEN: 24, // uncompressed size
+ CENNAM: 28, // filename length
+ CENEXT: 30, // extra field length
+ CENCOM: 32, // file comment length
+ CENDSK: 34, // volume number start
+ CENATT: 36, // internal file attributes
+ CENATX: 38, // external file attributes (host system dependent)
+ CENOFF: 42, // LOC header offset
+
+ /* The entries in the end of central directory */
+ ENDHDR: 22, // END header size
+ ENDSIG: 0x06054b50, // "PK\005\006"
+ ENDSIGFIRST: 0x50,
+ ENDSUB: 8, // number of entries on this disk
+ ENDTOT: 10, // total number of entries
+ ENDSIZ: 12, // central directory size in bytes
+ ENDOFF: 16, // offset of first CEN header
+ ENDCOM: 20, // zip file comment length
+ MAXFILECOMMENT: 0xffff,
+
+ /* The entries in the end of ZIP64 central directory locator */
+ ENDL64HDR: 20, // ZIP64 end of central directory locator header size
+ ENDL64SIG: 0x07064b50, // ZIP64 end of central directory locator signature
+ ENDL64SIGFIRST: 0x50,
+ ENDL64OFS: 8, // ZIP64 end of central directory offset
+
+ /* The entries in the end of ZIP64 central directory */
+ END64HDR: 56, // ZIP64 end of central directory header size
+ END64SIG: 0x06064b50, // ZIP64 end of central directory signature
+ END64SIGFIRST: 0x50,
+ END64SUB: 24, // number of entries on this disk
+ END64TOT: 32, // total number of entries
+ END64SIZ: 40,
+ END64OFF: 48,
+
+ /* Compression methods */
+ STORED: 0, // no compression
+ SHRUNK: 1, // shrunk
+ REDUCED1: 2, // reduced with compression factor 1
+ REDUCED2: 3, // reduced with compression factor 2
+ REDUCED3: 4, // reduced with compression factor 3
+ REDUCED4: 5, // reduced with compression factor 4
+ IMPLODED: 6, // imploded
+ // 7 reserved
+ DEFLATED: 8, // deflated
+ ENHANCED_DEFLATED: 9, // deflate64
+ PKWARE: 10, // PKWare DCL imploded
+ // 11 reserved
+ BZIP2: 12, // compressed using BZIP2
+ // 13 reserved
+ LZMA: 14, // LZMA
+ // 15-17 reserved
+ IBM_TERSE: 18, // compressed using IBM TERSE
+ IBM_LZ77: 19, //IBM LZ77 z
+
+ /* General purpose bit flag */
+ FLG_ENC: 0, // encrypted file
+ FLG_COMP1: 1, // compression option
+ FLG_COMP2: 2, // compression option
+ FLG_DESC: 4, // data descriptor
+ FLG_ENH: 8, // enhanced deflation
+ FLG_STR: 16, // strong encryption
+ FLG_LNG: 1024, // language encoding
+ FLG_MSK: 4096, // mask header values
+ FLG_ENTRY_ENC: 1,
+
+ /* 4.5 Extensible data fields */
+ EF_ID: 0,
+ EF_SIZE: 2,
+
+ /* Header IDs */
+ ID_ZIP64: 0x0001,
+ ID_AVINFO: 0x0007,
+ ID_PFS: 0x0008,
+ ID_OS2: 0x0009,
+ ID_NTFS: 0x000a,
+ ID_OPENVMS: 0x000c,
+ ID_UNIX: 0x000d,
+ ID_FORK: 0x000e,
+ ID_PATCH: 0x000f,
+ ID_X509_PKCS7: 0x0014,
+ ID_X509_CERTID_F: 0x0015,
+ ID_X509_CERTID_C: 0x0016,
+ ID_STRONGENC: 0x0017,
+ ID_RECORD_MGT: 0x0018,
+ ID_X509_PKCS7_RL: 0x0019,
+ ID_IBM1: 0x0065,
+ ID_IBM2: 0x0066,
+ ID_POSZIP: 0x4690,
+
+ EF_ZIP64_OR_32: 0xffffffff,
+ EF_ZIP64_OR_16: 0xffff,
+};
+
+class CentralDirectoryHeader {
+ volumeEntries!: number;
+ totalEntries!: number;
+ size!: number;
+ offset!: number;
+ commentLength!: number;
+ headerOffset!: number;
+
+ constructor(data: Buffer) {
+ if (data.length !== consts.ENDHDR || data.readUInt32LE(0) !== consts.ENDSIG) {
+ throw new Error('Invalid central directory');
+ }
+ // number of entries on this volume
+ this.volumeEntries = data.readUInt16LE(consts.ENDSUB);
+ // total number of entries
+ this.totalEntries = data.readUInt16LE(consts.ENDTOT);
+ // central directory size in bytes
+ this.size = data.readUInt32LE(consts.ENDSIZ);
+ // offset of first CEN header
+ this.offset = data.readUInt32LE(consts.ENDOFF);
+ // zip file comment length
+ this.commentLength = data.readUInt16LE(consts.ENDCOM);
+ }
+}
+
+class CentralDirectoryLoc64Header {
+ headerOffset!: number;
+
+ constructor(data: Buffer) {
+ if (data.length !== consts.ENDL64HDR || data.readUInt32LE(0) !== consts.ENDL64SIG) {
+ throw new Error('Invalid zip64 central directory locator');
+ }
+ // ZIP64 EOCD header offset
+ this.headerOffset = readUInt64LE(data, consts.ENDSUB);
+ }
+}
+
+class CentralDirectoryZip64Header {
+ volumeEntries!: number;
+ totalEntries!: number;
+ size!: number;
+ offset!: number;
+
+ constructor(data: Buffer) {
+ if (data.length !== consts.END64HDR || data.readUInt32LE(0) !== consts.END64SIG) {
+ throw new Error('Invalid central directory');
+ }
+ // number of entries on this volume
+ this.volumeEntries = readUInt64LE(data, consts.END64SUB);
+ // total number of entries
+ this.totalEntries = readUInt64LE(data, consts.END64TOT);
+ // central directory size in bytes
+ this.size = readUInt64LE(data, consts.END64SIZ);
+ // offset of first CEN header
+ this.offset = readUInt64LE(data, consts.END64OFF);
+ }
+}
+
+export class ZipEntry {
+ /**
+ * file name
+ */
+ name!: string;
+
+ /**
+ * true if it's a directory entry
+ */
+ isDirectory!: boolean;
+
+ /**
+ * file comment
+ */
+ comment!: string | null;
+
+ /**
+ * version made by
+ */
+ verMade: number;
+
+ /**
+ * version needed to extract
+ */
+ version: number;
+
+ /**
+ * encrypt, decrypt flags
+ */
+ flags: number;
+
+ /**
+ * compression method
+ */
+ method: number;
+
+ /**
+ * modification time
+ */
+ time: Date;
+
+ /**
+ * uncompressed file crc-32 value
+ */
+ crc: number;
+
+ /**
+ * compressed size
+ */
+ compressedSize: number;
+
+ /**
+ * uncompressed size
+ */
+ size: number;
+
+ /**
+ * volume number start
+ */
+ diskStart: number;
+
+ /**
+ * internal file attributes
+ */
+ inattr: number;
+
+ /**
+ * external file attributes
+ */
+ attr: number;
+
+ /**
+ * LOC header offset
+ */
+ offset: number;
+
+ fnameLen: number;
+ extraLen: number;
+ comLen: number;
+ headerOffset!: number;
+
+ constructor(private zipFile: ZipFile, data: Buffer, offset: number) {
+ // data should be 46 bytes and start with "PK 01 02"
+ if (data.length < offset + consts.CENHDR || data.readUInt32LE(offset) !== consts.CENSIG) {
+ throw new Error('Invalid entry header');
+ }
+ // version made by
+ this.verMade = data.readUInt16LE(offset + consts.CENVEM);
+ // version needed to extract
+ this.version = data.readUInt16LE(offset + consts.CENVER);
+ // encrypt, decrypt flags
+ this.flags = data.readUInt16LE(offset + consts.CENFLG);
+ // compression method
+ this.method = data.readUInt16LE(offset + consts.CENHOW);
+ // modification time (2 bytes time, 2 bytes date)
+ const timebytes = data.readUInt16LE(offset + consts.CENTIM);
+ const datebytes = data.readUInt16LE(offset + consts.CENTIM + 2);
+ this.time = parseZipTime(timebytes, datebytes);
+
+ // uncompressed file crc-32 value
+ this.crc = data.readUInt32LE(offset + consts.CENCRC);
+ // compressed size
+ this.compressedSize = data.readUInt32LE(offset + consts.CENSIZ);
+ // uncompressed size
+ this.size = data.readUInt32LE(offset + consts.CENLEN);
+ // filename length
+ this.fnameLen = data.readUInt16LE(offset + consts.CENNAM);
+ // extra field length
+ this.extraLen = data.readUInt16LE(offset + consts.CENEXT);
+ // file comment length
+ this.comLen = data.readUInt16LE(offset + consts.CENCOM);
+ // volume number start
+ this.diskStart = data.readUInt16LE(offset + consts.CENDSK);
+ // internal file attributes
+ this.inattr = data.readUInt16LE(offset + consts.CENATT);
+ // external file attributes
+ this.attr = data.readUInt32LE(offset + consts.CENATX);
+ // LOC header offset
+ this.offset = data.readUInt32LE(offset + consts.CENOFF);
+ }
+
+ read(): Promise {
+ return this.zipFile.openEntry(this);
+ }
+
+ parseDataHeader(data: Buffer) {
+ // 30 bytes and should start with "PK\003\004"
+ if (data.readUInt32LE(0) !== consts.LOCSIG) {
+ throw new Error('Invalid local header');
+ }
+ // version needed to extract
+ this.version = data.readUInt16LE(consts.LOCVER);
+ // general purpose bit flag
+ this.flags = data.readUInt16LE(consts.LOCFLG);
+ // compression method
+ this.method = data.readUInt16LE(consts.LOCHOW);
+ // modification time (2 bytes time ; 2 bytes date)
+ const timebytes = data.readUInt16LE(consts.LOCTIM);
+ const datebytes = data.readUInt16LE(consts.LOCTIM + 2);
+ this.time = parseZipTime(timebytes, datebytes);
+
+ // uncompressed file crc-32 value
+ this.crc = data.readUInt32LE(consts.LOCCRC) || this.crc;
+ // compressed size
+ const compressedSize = data.readUInt32LE(consts.LOCSIZ);
+ if (compressedSize && compressedSize !== consts.EF_ZIP64_OR_32) {
+ this.compressedSize = compressedSize;
+ }
+ // uncompressed size
+ const size = data.readUInt32LE(consts.LOCLEN);
+ if (size && size !== consts.EF_ZIP64_OR_32) {
+ this.size = size;
+ }
+ // filename length
+ this.fnameLen = data.readUInt16LE(consts.LOCNAM);
+ // extra field length
+ this.extraLen = data.readUInt16LE(consts.LOCEXT);
+ }
+
+ processFilename(data: Buffer, offset: number) {
+ this.name = data.slice(offset, (offset += this.fnameLen)).toString();
+ const lastChar = data[offset - 1];
+ this.isDirectory = lastChar === 47 || lastChar === 92;
+
+ if (this.extraLen) {
+ this.readExtra(data, offset);
+ offset += this.extraLen;
+ }
+ this.comment = this.comLen ? data.slice(offset, offset + this.comLen).toString() : null;
+ }
+
+ validateName() {
+ if (/\\|^\w+:|^\/|(^|\/)\.\.(\/|$)/.test(this.name)) {
+ throw new Error('Malicious entry: ' + this.name);
+ }
+ }
+
+ readExtra(data: Buffer, offset: number) {
+ let signature, size;
+ const maxPos = offset + this.extraLen;
+ while (offset < maxPos) {
+ signature = data.readUInt16LE(offset);
+ offset += 2;
+ size = data.readUInt16LE(offset);
+ offset += 2;
+ if (consts.ID_ZIP64 === signature) {
+ this.parseZip64Extra(data, offset, size);
+ }
+ offset += size;
+ }
+ }
+
+ parseZip64Extra(data: Buffer, offset: number, length: number) {
+ if (length >= 8 && this.size === consts.EF_ZIP64_OR_32) {
+ this.size = readUInt64LE(data, offset);
+ offset += 8;
+ length -= 8;
+ }
+ if (length >= 8 && this.compressedSize === consts.EF_ZIP64_OR_32) {
+ this.compressedSize = readUInt64LE(data, offset);
+ offset += 8;
+ length -= 8;
+ }
+ if (length >= 8 && this.offset === consts.EF_ZIP64_OR_32) {
+ this.offset = readUInt64LE(data, offset);
+ offset += 8;
+ length -= 8;
+ }
+ if (length >= 4 && this.diskStart === consts.EF_ZIP64_OR_16) {
+ this.diskStart = data.readUInt32LE(offset);
+ // offset += 4; length -= 4;
+ }
+ }
+
+ get encrypted() {
+ return (this.flags & consts.FLG_ENTRY_ENC) === consts.FLG_ENTRY_ENC;
+ }
+
+ get isFile() {
+ return !this.isDirectory;
+ }
+}
+
+class FileWindowBuffer {
+ position = 0;
+ buffer = Buffer.alloc(0);
+ constructor(public readHandle: ReadHandle) {
+ }
+
+ async read(pos: number, length: number) {
+ if (this.buffer.length < length) {
+ this.buffer = Buffer.alloc(length);
+ }
+ this.position = pos;
+ await this.readHandle.readComplete(this.buffer, 0, length, this.position);
+ }
+
+ async expandLeft(length: number) {
+ this.buffer = Buffer.concat([Buffer.alloc(length), this.buffer]);
+ this.position -= length;
+ if (this.position < 0) {
+ this.position = 0;
+ }
+ await this.readHandle.readComplete(this.buffer, 0, length, this.position);
+ }
+
+ async expandRight(length: number,) {
+ const offset = this.buffer.length;
+ this.buffer = Buffer.concat([this.buffer, Buffer.alloc(length)]);
+ await this.readHandle.readComplete(this.buffer, offset, length, this.position + offset);
+ }
+
+ async moveRight(shift: number) {
+ if (shift) {
+ this.buffer.copy(this.buffer, 0, shift);
+ }
+ this.position += shift;
+ await this.readHandle.readComplete(this.buffer, this.buffer.length - shift, shift, this.position + this.buffer.length - shift);
+ }
+}
+
+function createVerifier(crc: number, size: number) {
+ const verify = new Verifier(crc, size);
+ return new Transform({
+ transform: (data: any, unused: BufferEncoding, passThru: TransformCallback) => {
+ let err;
+ try {
+ verify.data(data);
+ } catch (e: any) {
+ err = e;
+ }
+ passThru(err, data);
+ }
+ });
+}
+
+function createCrcTable() {
+ const crcTable = [];
+ const b = Buffer.alloc(4);
+ for (let n = 0; n < 256; n++) {
+ let c = n;
+ for (let k = 8; --k >= 0;) {
+ if ((c & 1) !== 0) {
+ c = 0xedb88320 ^ (c >>> 1);
+ } else {
+ c = c >>> 1;
+ }
+ }
+ if (c < 0) {
+ b.writeInt32LE(c, 0);
+ c = b.readUInt32LE(0);
+ }
+ crcTable[n] = c;
+ }
+ return crcTable;
+}
+
+const crcTable = createCrcTable();
+
+class Verifier {
+ state = {
+ crc: ~0,
+ size: 0,
+ };
+
+ constructor(public crc: number, public size: number) {
+ }
+
+ data(data: Buffer) {
+ let crc = this.state.crc;
+ let off = 0;
+ let len = data.length;
+ while (--len >= 0) {
+ crc = crcTable[(crc ^ data[off++]) & 0xff] ^ (crc >>> 8);
+ }
+ this.state.crc = crc;
+ this.state.size += data.length;
+ if (this.state.size >= this.size) {
+ const buf = Buffer.alloc(4);
+ buf.writeInt32LE(~this.state.crc & 0xffffffff, 0);
+ crc = buf.readUInt32LE(0);
+ if (crc !== this.crc) {
+ throw new Error(`Invalid CRC Expected: ${this.crc} found:${crc} `);
+ }
+ if (this.state.size !== this.size) {
+ throw new Error('Invalid size');
+ }
+ }
+ }
+}
+
+function parseZipTime(timebytes: number, datebytes: number) {
+ const timebits = toBits(timebytes, 16);
+ const datebits = toBits(datebytes, 16);
+
+ const mt = {
+ h: parseInt(timebits.slice(0, 5).join(''), 2),
+ m: parseInt(timebits.slice(5, 11).join(''), 2),
+ s: parseInt(timebits.slice(11, 16).join(''), 2) * 2,
+ Y: parseInt(datebits.slice(0, 7).join(''), 2) + 1980,
+ M: parseInt(datebits.slice(7, 11).join(''), 2),
+ D: parseInt(datebits.slice(11, 16).join(''), 2),
+ };
+ const dt_str = [mt.Y, mt.M, mt.D].join('-') + ' ' + [mt.h, mt.m, mt.s].join(':') + ' GMT+0';
+ return new Date(dt_str);
+}
+
+function toBits(dec: number, size: number) {
+ let b = (dec >>> 0).toString(2);
+ while (b.length < size) {
+ b = '0' + b;
+ }
+ return b.split('');
+}
+
+function readUInt64LE(buffer: Buffer, offset: number) {
+ return buffer.readUInt32LE(offset + 4) * 0x0000000100000000 + buffer.readUInt32LE(offset);
+}
diff --git a/ce/ce/artifacts/SetOfDemands.ts b/ce/ce/artifacts/SetOfDemands.ts
new file mode 100644
index 0000000000..58d3ac41e3
--- /dev/null
+++ b/ce/ce/artifacts/SetOfDemands.ts
@@ -0,0 +1,86 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { MetadataFile } from '../amf/metadata-file';
+import { Demands } from '../interfaces/metadata/demands';
+import { VersionReference } from '../interfaces/metadata/version-reference';
+import { parseQuery } from '../mediaquery/media-query';
+import { Session } from '../session';
+import { MultipleInstallsMatched } from '../util/exceptions';
+import { Dictionary, linq } from '../util/linq';
+import { Activation } from './activation';
+
+
+export class SetOfDemands {
+ _demands = new Map();
+
+ constructor(metadata: MetadataFile, session: Session) {
+ this._demands.set('', metadata);
+
+ for (const [query, demands] of metadata.conditionalDemands) {
+ if (parseQuery(query).match(session.context)) {
+ session.channels.debug(`Matching demand query: '${query}'`);
+ this._demands.set(query, demands);
+ }
+ }
+ }
+
+ setActivation(activation: Activation) {
+ for (const [, demandBlock] of this._demands.entries()) {
+ demandBlock.setActivation(activation);
+ }
+ }
+
+ /** Async Initializer */
+ async init(session: Session) {
+ for (const [query, demands] of this._demands) {
+ await demands.init(session);
+ }
+ }
+
+ get installer() {
+ const install = linq.entries(this._demands).where(([query, demand]) => demand.install.length > 0).toArray();
+
+ if (install.length > 1) {
+ // bad. There should only ever be one install block.
+ throw new MultipleInstallsMatched(install.map(each => each[0]));
+ }
+
+ return install[0]?.[1].install || [];
+ }
+
+ get errors() {
+ return linq.values(this._demands).selectNonNullable(d => d.error).toArray();
+ }
+ get warnings() {
+ return linq.values(this._demands).selectNonNullable(d => d.warning).toArray();
+ }
+ get messages() {
+ return linq.values(this._demands).selectNonNullable(d => d.message).toArray();
+ }
+ get settings() {
+ return linq.values(this._demands).selectNonNullable(d => d.settings).toArray();
+ }
+ get seeAlso() {
+ return linq.values(this._demands).selectNonNullable(d => d.seeAlso).toArray();
+ }
+
+ get requires() {
+ const d = this._demands;
+ const rq1 = linq.values(d).selectNonNullable(d => d.requires).toArray();
+ const result = new Dictionary();
+ for (const dict of rq1) {
+ for (const [query, demands] of dict) {
+ result[query] = demands;
+ }
+ }
+ const rq = [...d.values()].map(each => each.requires).filter(each => each);
+
+ for (const dict of rq) {
+ for (const [query, demands] of dict) {
+ result[query] = demands;
+ }
+ }
+ return result;
+ }
+}
diff --git a/ce/ce/artifacts/activation.ts b/ce/ce/artifacts/activation.ts
new file mode 100644
index 0000000000..acd4611a32
--- /dev/null
+++ b/ce/ce/artifacts/activation.ts
@@ -0,0 +1,166 @@
+/* eslint-disable keyword-spacing */
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { delimiter } from 'path';
+import { Session } from '../session';
+import { linq } from '../util/linq';
+import { Uri } from '../util/uri';
+import { toXml } from '../util/xml';
+import { Artifact } from './artifact';
+
+export class Activation {
+ #session: Session;
+ constructor(session: Session) {
+ this.#session = session;
+ }
+
+ /** gets a flattend object representation of the activation */
+ get output() {
+ return {
+ defines: Object.fromEntries(this.defines),
+ locations: Object.fromEntries([... this.locations.entries()].map(([k, v]) => [k, v.fsPath])),
+ properties: Object.fromEntries([... this.properties.entries()].map(([k, v]) => [k, v.join(',')])),
+ environment: { ...process.env, ...Object.fromEntries([... this.environment.entries()].map(([k, v]) => [k, v.join(' ')])) },
+ tools: Object.fromEntries(this.tools),
+ paths: Object.fromEntries([...this.paths.entries()].map(([k, v]) => [k, v.map(each => each.fsPath).join(delimiter)])),
+ aliases: Object.fromEntries(this.aliases)
+ };
+ }
+
+ generateMSBuild(artifacts: Iterable): string {
+ const msbuildFile = {
+ Project: {
+ $xmlns: 'http://schemas.microsoft.com/developer/msbuild/2003',
+ PropertyGroup: >>[]
+ }
+ };
+
+ if (this.locations.size) {
+ msbuildFile.Project.PropertyGroup.push({ $Label: 'Locations', ...linq.entries(this.locations).toObject(([key, value]) => [key, value.fsPath]) });
+ }
+
+ if (this.properties.size) {
+ msbuildFile.Project.PropertyGroup.push({ $Label: 'Properties', ...linq.entries(this.properties).toObject(([key, value]) => [key, value.join(';')]) });
+ }
+
+ if (this.tools.size) {
+ msbuildFile.Project.PropertyGroup.push({ $Label: 'Tools', ...linq.entries(this.tools).toObject(each => each) });
+ }
+
+ if (this.environment.size) {
+ msbuildFile.Project.PropertyGroup.push({ $Label: 'Environment', ...linq.entries(this.environment).toObject(each => each) });
+ }
+
+ if (this.paths.size) {
+ msbuildFile.Project.PropertyGroup.push({ $Label: 'Paths', ...linq.entries(this.paths).toObject(([key, value]) => [key, value.map(each => each.fsPath).join(';')]) });
+ }
+
+ if (this.defines.size) {
+ msbuildFile.Project.PropertyGroup.push({ $Label: 'Defines', DEFINES: linq.entries(this.defines).select(([key, value]) => `${key}=${value}`).join(';') });
+ }
+
+ if (this.aliases.size) {
+ msbuildFile.Project.PropertyGroup.push({ $Label: 'Aliases', ...linq.entries(this.environment).toObject(each => each) });
+ }
+
+ const propertyGroup = { $Label: 'Artifacts', Artifacts: { Artifact: [] } };
+
+ for (const artifact of artifacts) {
+ propertyGroup.Artifacts.Artifact.push({ $id: artifact.metadata.info.id, '#text': artifact.targetLocation.fsPath });
+ }
+
+ if (propertyGroup.Artifacts.Artifact.length > 0) {
+ msbuildFile.Project.PropertyGroup.push(propertyGroup);
+ }
+
+ return toXml(msbuildFile);
+ }
+
+ /** a collection of #define declarations that would assumably be applied to all compiler calls. */
+ defines = new Map();
+
+ /** a collection of tool definitions from artifacts (think shell 'aliases') */
+ tools = new Map();
+
+ /** Aliases are tools that get exposed to the user as shell aliases */
+ aliases = new Map();
+
+ /** a collection of 'published locations' from artifacts. useful for msbuild */
+ locations = new Map();
+
+ /** a collection of environment variables from artifacts that are intended to be combinined into variables that have PATH delimiters */
+ paths = new Map>();
+
+ /** environment variables from artifacts */
+ environment = new Map>();
+
+ /** a collection of arbitrary properties from artifacts. useful for msbuild */
+ properties = new Map>();
+
+ get Paths() {
+ // return just paths that have contents.
+ return [... this.paths.entries()].filter(([k, v]) => v.length > 0);
+ }
+
+ get Variables() {
+ // tools + environment
+ const result = new Array<[string, string]>();
+
+ // combine variables with spaces
+ for (const [key, values] of this.environment) {
+ result.push([key, values.join(' ')]);
+ }
+
+ // add tools to the list
+ for (const [key, value] of this.tools) {
+ result.push([key, value]);
+ }
+ return result;
+ }
+
+ get Defines(): Array<[string, string]> {
+ return linq.entries(this.defines).toArray();
+ }
+
+ get Locations(): Array<[string, string]> {
+ return linq.entries(this.locations).select(([k, v]) => <[string, string]>[k, v.fsPath]).where(([k, v]) => v.length > 0).toArray();
+ }
+
+ get Properties(): Array<[string, Array]> {
+ return linq.entries(this.properties).toArray();
+ }
+
+ /** produces an environment block that can be passed to child processes to leverage dependent artifacts during installtion/activation. */
+ get environmentBlock(): NodeJS.ProcessEnv {
+ const result = this.#session.environment;
+
+ // add environment variables
+ for (const [k, v] of this.Variables) {
+ result[k] = v;
+ }
+
+ // update environment paths
+ for (const [variable, values] of this.Paths) {
+ if (values.length) {
+ const s = new Set(values.map(each => each.fsPath));
+ const originalVariable = result[variable] || '';
+ if (originalVariable) {
+ for (const p of originalVariable.split(delimiter)) {
+ if (p) {
+ s.add(p);
+ }
+ }
+ }
+ result[variable] = originalVariable;
+ }
+ }
+
+ // define tool environment variables
+ for (const [key, value] of this.tools) {
+ result[key] = value;
+ }
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/ce/ce/artifacts/artifact.ts b/ce/ce/artifacts/artifact.ts
new file mode 100644
index 0000000000..e493f685ff
--- /dev/null
+++ b/ce/ce/artifacts/artifact.ts
@@ -0,0 +1,358 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { fail } from 'assert';
+import { resolve } from 'path';
+import { MetadataFile } from '../amf/metadata-file';
+import { gitArtifact, gitUniqueIdPrefix, latestVersion } from '../constants';
+import { i } from '../i18n';
+import { InstallEvents } from '../interfaces/events';
+import { Registries } from '../registries/registries';
+import { Session } from '../session';
+import { linq } from '../util/linq';
+import { Uri } from '../util/uri';
+import { Activation } from './activation';
+import { Registry } from './registry';
+import { SetOfDemands } from './SetOfDemands';
+
+export type Selections = Map;
+export type UID = string;
+export type ID = string;
+export type VersionRange = string;
+export type Selection = [Artifact, ID, VersionRange]
+
+export class ArtifactMap extends Map{
+ get artifacts() {
+ return [...linq.values(this).select(([artifact, id, range]) => artifact)].sort((a, b) => (b.metadata.info.priority || 0) - (a.metadata.info.priority || 0));
+ }
+}
+
+class ArtifactBase {
+ public registries: Registries;
+ readonly applicableDemands: SetOfDemands;
+
+ constructor(protected session: Session, public readonly metadata: MetadataFile) {
+ this.applicableDemands = new SetOfDemands(this.metadata, this.session);
+ this.registries = new Registries(session);
+
+ // load the registries from the project file
+ for (const [name, registry] of this.metadata.registries) {
+ const reg = session.loadRegistry(registry.location.get(0), registry.registryKind || 'artifact');
+ if (reg) {
+ this.registries.add(reg, name);
+ }
+ }
+ }
+
+ /** Async Initializer */
+ async init(session: Session) {
+ await this.applicableDemands.init(session);
+ return this;
+ }
+
+ async resolveDependencies(artifacts = new ArtifactMap(), recurse = true) {
+ // find the dependencies and add them to the set
+
+ let dependency: [Registry, string, Artifact] | undefined;
+ for (const [id, version] of linq.entries(this.applicableDemands.requires)) {
+ dependency = undefined;
+ if (id.indexOf(':') === -1) {
+ if (this.metadata.registry) {
+ // let's assume the dependency is in the same registry as the artifact
+ const [registry, b, artifacts] = (await this.metadata.registry.search(this.registries, { idOrShortName: id, version: version.raw }))[0];
+ dependency = [registry, b, artifacts[0]];
+ if (!dependency) {
+ throw new Error(i`Dependency '${id}' version '${version.raw}' does not specify the registry.`);
+ }
+ }
+ }
+ dependency = dependency || await this.registries.getArtifact(id, version.raw);
+ if (!dependency) {
+ throw new Error(i`Unable to resolve dependency ${id}: ${version.raw}`);
+ }
+ const artifact = dependency[2];
+ if (!artifacts.has(artifact.uniqueId)) {
+ artifacts.set(artifact.uniqueId, [artifact, id, version.raw || latestVersion]);
+
+ if (recurse) {
+ // process it's dependencies too.
+ await artifact.resolveDependencies(artifacts);
+ }
+ }
+ }
+
+
+ if (!linq.startsWith(artifacts, gitUniqueIdPrefix)) {
+ // check if anyone needs git and add it if it isn't there
+ for (const each of this.applicableDemands.installer) {
+ if (each.installerKind === 'git') {
+ const [reg, id, art] = await this.registries.getArtifact(gitArtifact, latestVersion) || [];
+ if (art) {
+ artifacts.set(gitArtifact, [art, gitArtifact, latestVersion]);
+ break;
+ }
+ }
+ }
+ }
+ return artifacts;
+ }
+}
+
+export class Artifact extends ArtifactBase {
+ isPrimary = false;
+
+ constructor(session: Session, metadata: MetadataFile, public shortName: string = '', public targetLocation: Uri, public readonly registryId: string, public readonly registryUri: Uri) {
+ super(session, metadata);
+ }
+
+ get id() {
+ return this.metadata.info.id;
+ }
+
+ get reference() {
+ return `${this.registryId}:${this.id}`;
+ }
+
+ get version() {
+ return this.metadata.info.version;
+ }
+
+ get isInstalled() {
+ return this.targetLocation.exists('artifact.yaml');
+ }
+
+ get uniqueId() {
+ return `${this.registryUri.toString()}::${this.id}::${this.version}`;
+ }
+
+ async install(activation: Activation, events: Partial, options: { force?: boolean, allLanguages?: boolean, language?: string }): Promise {
+ let installing = false;
+ try {
+ // is it installed?
+ const applicableDemands = this.applicableDemands;
+ applicableDemands.setActivation(activation);
+
+ let isFailing = false;
+ for (const error of applicableDemands.errors) {
+ this.session.channels.error(error);
+ isFailing = true;
+ }
+
+ if (isFailing) {
+ throw Error('errors present');
+ }
+
+ // warnings
+ for (const warning of applicableDemands.warnings) {
+ this.session.channels.warning(warning);
+ }
+
+ // messages
+ for (const message of applicableDemands.messages) {
+ this.session.channels.message(message);
+ }
+
+ if (await this.isInstalled && !options.force) {
+ await this.loadActivationSettings(activation);
+ return false;
+ }
+ installing = true;
+
+ if (options.force) {
+ try {
+ await this.uninstall();
+ } catch {
+ // if a file is locked, it may not get removed. We'll deal with this later.
+ }
+ }
+
+ // ok, let's install this.
+ for (const installInfo of applicableDemands.installer) {
+ if (installInfo.lang && !options.allLanguages && options.language && options.language.toLowerCase() !== installInfo.lang.toLowerCase()) {
+ continue;
+ }
+
+ const installer = this.session.artifactInstaller(installInfo);
+ if (!installer) {
+ fail(i`Unknown installer type ${installInfo!.installerKind}`);
+ }
+ await installer(this.session, activation, this.id, this.targetLocation, installInfo, events, options);
+ }
+
+ // after we unpack it, write out the installed manifest
+ await this.writeManifest();
+ await this.loadActivationSettings(activation);
+ return true;
+ } catch (err) {
+ if (installing) {
+ // if we started installing, and then had an error, we need to remove the artifact.
+ try {
+ await this.uninstall();
+ } catch {
+ // if a file is locked, it may not get removed. We'll deal with this later.
+ }
+ }
+ throw err;
+ }
+ }
+
+ get name() {
+ return `${this.metadata.info.id.replace(/[^\w]+/g, '.')}-${this.metadata.info.version}`;
+ }
+
+ async writeManifest() {
+ await this.targetLocation.createDirectory();
+ await this.metadata.save(this.targetLocation.join('artifact.yaml'));
+ }
+
+ async uninstall() {
+ await this.targetLocation.delete({ recursive: true, useTrash: false });
+ }
+
+ async loadActivationSettings(activation: Activation) {
+ // construct paths (bin, lib, include, etc.)
+ // construct tools
+ // compose variables
+ // defines
+
+ const l = this.targetLocation.toString().length + 1;
+ const allPaths = (await this.targetLocation.readDirectory(undefined, { recursive: true })).select(([name, stat]) => name.toString().substr(l));
+
+ for (const settingBlock of this.applicableDemands.settings) {
+ // **** defines ****
+ // eslint-disable-next-line prefer-const
+ for (let [key, value] of settingBlock.defines) {
+ if (value === 'true') {
+ value = '1';
+ }
+
+ const v = activation.defines.get(key);
+ if (v && v !== value) {
+ // conflict. todo: what do we want to do?
+ this.session.channels.warning(i`Duplicate define ${key} during activation. New value will replace old `);
+ }
+ activation.defines.set(key, value);
+ }
+
+ // **** paths ****
+ for (const key of settingBlock.paths.keys) {
+ if (!key) {
+ continue;
+ }
+
+ const pathEnvVariable = key.toUpperCase();
+ const p = activation.paths.getOrDefault(pathEnvVariable, []);
+ const l = settingBlock.paths.get(key);
+ const uris = new Set();
+
+ for (const location of l ?? []) {
+ // check that each path is an actual path.
+ const path = await this.sanitizeAndValidatePath(location);
+ if (path && !uris.has(path)) {
+ uris.add(path);
+ p.push(path);
+ }
+ }
+ }
+
+ // **** tools ****
+ for (const key of settingBlock.tools.keys) {
+ const envVariable = key.toUpperCase();
+
+ if (activation.tools.has(envVariable)) {
+ this.session.channels.error(i`Duplicate tool declared ${key} during activation. Probably not a good thing?`);
+ }
+
+ const p = settingBlock.tools.get(key) || '';
+ const uri = await this.sanitizeAndValidatePath(p);
+ if (uri) {
+ activation.tools.set(envVariable, uri.fsPath);
+ } else {
+ if (p) {
+ activation.tools.set(envVariable, p);
+ // this.session.channels.warning(i`Invalid tool path '${p}'`);
+ }
+ }
+ }
+
+ // **** variables ****
+ for (const [key, value] of settingBlock.variables) {
+ const envKey = activation.environment.getOrDefault(key, []);
+ envKey.push(...value);
+ }
+
+ // **** properties ****
+ for (const [key, value] of settingBlock.properties) {
+ const envKey = activation.properties.getOrDefault(key, []);
+ envKey.push(...value);
+ }
+
+ // **** locations ****
+ for (const locationName of settingBlock.locations.keys) {
+ if (activation.locations.has(locationName)) {
+ this.session.channels.error(i`Duplicate location declared ${locationName} during activation. Probably not a good thing?`);
+ }
+
+ const p = settingBlock.locations.get(locationName) || '';
+ const uri = await this.sanitizeAndValidatePath(p);
+ if (uri) {
+ activation.locations.set(locationName, uri);
+ }
+ }
+ }
+ }
+
+ async sanitizeAndValidatePath(path: string) {
+ if (!path.startsWith('.')) {
+ try {
+ const loc = this.session.fileSystem.file(resolve(path));
+ if (await loc.exists()) {
+ return loc;
+ }
+ } catch {
+ // no worries, treat it like a relative path.
+ }
+ }
+ const loc = this.targetLocation.join(sanitizePath(path));
+ if (await loc.exists()) {
+ return loc;
+ }
+ return undefined;
+ }
+}
+
+export function sanitizePath(path: string) {
+ return path.
+ replace(/[\\/]+/g, '/'). // forward slahses please
+ replace(/[?<>:|"]/g, ''). // remove illegal characters.
+ // eslint-disable-next-line no-control-regex
+ replace(/[\x00-\x1f\x80-\x9f]/g, ''). // remove unicode control codes
+ replace(/^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i, ''). // no reserved names
+ replace(/^[/.]*\//, ''). // dots and slashes off the front.
+ replace(/[/.]+$/, ''). // dots and slashes off the back.
+ replace(/\/\.+\//g, '/'). // no parts made just of dots.
+ replace(/\/+/g, '/'); // duplicate slashes.
+}
+
+export function sanitizeUri(u: string) {
+ return u.
+ replace(/[\\/]+/g, '/'). // forward slahses please
+ replace(/[?<>|"]/g, ''). // remove illegal characters.
+ // eslint-disable-next-line no-control-regex
+ replace(/[\x00-\x1f\x80-\x9f]/g, ''). // remove unicode control codes
+ replace(/^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i, ''). // no reserved names
+ replace(/^[/.]*\//, ''). // dots and slashes off the front.
+ replace(/[/.]+$/, ''). // dots and slashes off the back.
+ replace(/\/\.+\//g, '/'). // no parts made just of dots.
+ replace(/\/+/g, '/'); // duplicate slashes.
+}
+
+export class ProjectManifest extends ArtifactBase {
+
+}
+
+export class InstalledArtifact extends Artifact {
+ constructor(session: Session, metadata: MetadataFile) {
+ super(session, metadata, '', Uri.invalid, 'OnDisk?', Uri.invalid); /* fixme ? */
+ }
+}
\ No newline at end of file
diff --git a/ce/ce/artifacts/registry.ts b/ce/ce/artifacts/registry.ts
new file mode 100644
index 0000000000..bb7d0c5f74
--- /dev/null
+++ b/ce/ce/artifacts/registry.ts
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { Registries } from '../registries/registries';
+import { Uri } from '../util/uri';
+import { Artifact } from './artifact';
+
+export interface SearchCriteria {
+ idOrShortName?: string;
+ version?: string
+ keyword?: string;
+}
+
+export interface Registry {
+ readonly count: number;
+ readonly location: Uri;
+ readonly loaded: boolean;
+
+ search(parent: Registries, criteria?: SearchCriteria): Promise]>>;
+
+ load(force?: boolean): Promise;
+ save(): Promise;
+ update(): Promise;
+ regenerate(): Promise;
+}
diff --git a/ce/ce/cli/argument.ts b/ce/ce/cli/argument.ts
new file mode 100644
index 0000000000..970022fc97
--- /dev/null
+++ b/ce/ce/cli/argument.ts
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { Command } from './command';
+import { Help } from './command-line';
+
+export abstract class Argument implements Help {
+ readonly abstract argument: string;
+ readonly title = '';
+ readonly abstract help: Array;
+
+ constructor(protected command: Command) {
+ command.arguments.push(this);
+ }
+}
diff --git a/ce/ce/cli/artifacts.ts b/ce/ce/cli/artifacts.ts
new file mode 100644
index 0000000000..08782b7a62
--- /dev/null
+++ b/ce/ce/cli/artifacts.ts
@@ -0,0 +1,153 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { MultiBar, SingleBar } from 'cli-progress';
+import { Activation } from '../artifacts/activation';
+import { Artifact, ArtifactMap } from '../artifacts/artifact';
+import { i } from '../i18n';
+import { trackAcquire } from '../insights';
+import { Registries } from '../registries/registries';
+import { Session } from '../session';
+import { artifactIdentity, artifactReference } from './format';
+import { Table } from './markdown-table';
+import { debug, error, log } from './styling';
+
+export async function showArtifacts(artifacts: Iterable, options?: { force?: boolean }) {
+ let failing = false;
+ const table = new Table(i`Artifact`, i`Version`, i`Status`, i`Dependency`, i`Summary`);
+ for (const artifact of artifacts) {
+ const name = artifactIdentity(artifact.registryId, artifact.id, artifact.shortName);
+ if (!artifact.metadata.isValid) {
+ failing = true;
+ for (const err of artifact.metadata.validationErrors) {
+ error(err);
+ }
+ }
+ table.push(name, artifact.version, options?.force || await artifact.isInstalled ? 'installed' : 'will install', artifact.isPrimary ? ' ' : '*', artifact.metadata.info.summary || '');
+ }
+ log(table.toString());
+
+ return !failing;
+}
+
+export type Selections = Map;
+
+export async function selectArtifacts(selections: Selections, registries: Registries): Promise {
+ const artifacts = new ArtifactMap();
+
+ for (const [identity, version] of selections) {
+ const [registry, id, artifact] = await registries.getArtifact(identity, version) || [];
+
+ if (!artifact) {
+ error(`Unable to resolve artifact: ${artifactReference('', identity, version)}`);
+ return false;
+ }
+
+ artifacts.set(artifact.uniqueId, [artifact, identity, version]);
+ artifact.isPrimary = true;
+ await artifact.resolveDependencies(artifacts);
+ }
+ return artifacts;
+}
+
+export async function installArtifacts(session: Session, artifacts: Iterable, options?: { force?: boolean, allLanguages?: boolean, language?: string }): Promise<[boolean, Map, Activation]> {
+ // resolve the full set of artifacts to install.
+ const installed = new Map();
+ const activation = new Activation(session);
+
+ const bar = new MultiBar({
+ clearOnComplete: true, hideCursor: true, format: '{name} {bar}\u25A0 {percentage}% {action} {current}',
+ barCompleteChar: '\u25A0',
+ barIncompleteChar: ' ',
+ etaBuffer: 40
+ });
+ let dl: SingleBar | undefined;
+ let p: SingleBar | undefined;
+ let spinnerValue = 0;
+
+ for (const artifact of artifacts) {
+ const id = artifact.id;
+ const registryName = artifact.registryId;
+
+ try {
+ const actuallyInstalled = await artifact.install(activation, {
+ verifying: (name, percent) => {
+ if (percent >= 100) {
+ p?.update(percent);
+ p = undefined;
+ return;
+ }
+ if (percent) {
+ if (!p) {
+ p = bar.create(100, 0, { action: i`verifying`, name: artifactIdentity(registryName, id), current: name });
+ }
+ p?.update(percent);
+ }
+ },
+ download: (name, percent) => {
+ if (percent >= 100) {
+ if (dl) {
+ dl.update(percent);
+ }
+ dl = undefined;
+ return;
+ }
+ if (percent) {
+ if (!dl) {
+ dl = bar.create(100, 0, { action: i`downloading`, name: artifactIdentity(registryName, id), current: name });
+ }
+ dl.update(percent);
+ }
+ },
+ fileProgress: (entry) => {
+ p?.update({ action: i`unpacking`, name: artifactIdentity(registryName, id), current: entry.extractPath });
+ },
+ progress: (percent: number) => {
+ if (percent >= 100) {
+ if (p) {
+ p.update(percent, { action: i`unpacked`, name: artifactIdentity(registryName, id), current: '' });
+ }
+ p = undefined;
+ return;
+ }
+ if (percent) {
+ if (!p) {
+ p = bar.create(100, 0, { action: i`unpacking`, name: artifactIdentity(registryName, id), current: '' });
+ }
+ p.update(percent);
+ }
+ },
+ heartbeat: (text: string) => {
+ if (!p) {
+ p = bar.create(3, 0, { action: i`working`, name: artifactIdentity(registryName, id), current: '' });
+ }
+ p?.update((spinnerValue++) % 4, { action: i`working`, name: artifactIdentity(registryName, id), current: text });
+ }
+ }, options || {});
+ // remember what was actually installed
+ installed.set(artifact, actuallyInstalled);
+ if (actuallyInstalled) {
+ trackAcquire(artifact.id, artifact.version);
+ }
+ } catch (e: any) {
+ bar.stop();
+ debug(e);
+ debug(e.stack);
+ error(i`Error installing ${artifactIdentity(registryName, id)} - ${e} `);
+ return [false, installed, activation];
+ }
+
+ bar.stop();
+ }
+ return [true, installed, activation];
+}
+
+export async function activateArtifacts(session: Session, artifacts: Iterable) {
+ const activation = new Activation(session);
+ for (const artifact of artifacts) {
+ if (await artifact.isInstalled) {
+ await artifact.loadActivationSettings(activation);
+ }
+ }
+ return activation;
+}
diff --git a/ce/ce/cli/command-line.ts b/ce/ce/cli/command-line.ts
new file mode 100644
index 0000000000..48675af6b3
--- /dev/null
+++ b/ce/ce/cli/command-line.ts
@@ -0,0 +1,167 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { strict } from 'assert';
+import { tmpdir } from 'os';
+import { join, resolve } from 'path';
+import { i } from '../i18n';
+import { intersect } from '../util/intersect';
+import { Command } from './command';
+import { cmdSwitch } from './format';
+
+export type switches = {
+ [key: string]: Array;
+}
+
+export interface Help {
+ readonly help: Array;
+ readonly title: string;
+}
+
+class Ctx {
+ constructor(cmdline: CommandLine) {
+ this.os =
+ cmdline.isSet('windows') ? 'win32' :
+ cmdline.isSet('osx') ? 'darwin' :
+ cmdline.isSet('linux') ? 'linux' :
+ cmdline.isSet('freebsd') ? 'freebsd' :
+ process.platform;
+ this.arch = cmdline.isSet('x64') ? 'x64' :
+ cmdline.isSet('x86') ? 'x32' :
+ cmdline.isSet('arm') ? 'arm' :
+ cmdline.isSet('arm64') ? 'arm64' :
+ process.arch;
+ }
+
+ readonly os: string;
+ readonly arch: string;
+
+ get windows(): boolean {
+ return this.os === 'win32';
+ }
+
+ get linux(): boolean {
+ return this.os === 'linux';
+ }
+
+ get freebsd(): boolean {
+ return this.os === 'freebsd';
+ }
+
+ get osx(): boolean {
+ return this.os === 'darwin';
+ }
+
+ get x64(): boolean {
+ return this.arch === 'x64';
+ }
+
+ get x86(): boolean {
+ return this.arch === 'x32';
+ }
+
+ get arm(): boolean {
+ return this.arch === 'arm';
+ }
+
+ get arm64(): boolean {
+ return this.arch === 'arm64';
+ }
+}
+
+export function resolvePath(v: string | undefined) {
+ return v?.startsWith('.') ? resolve(v) : v;
+}
+
+export class CommandLine {
+ readonly commands = new Array();
+ readonly inputs = new Array();
+ readonly switches: switches = {};
+ readonly context: Ctx & switches;
+
+ #home?: string;
+ get homeFolder() {
+ // home folder is determined by
+ // command line (--vcpkg_root, --vcpkg-root )
+ // environment (VCPKG_ROOT)
+ // default 1 $HOME/.vcpkg
+ // default 2 /.vcpkg
+
+ // note, this does not create the folder, that would happen when the session is initialized.
+
+ return this.#home || (this.#home = resolvePath(
+ this.switches['vcpkg-root']?.[0] ||
+ this.switches['vcpkg_root']?.[0] ||
+ process.env['VCPKG_ROOT'] ||
+ join(process.env['HOME'] || process.env['USERPROFILE'] || tmpdir(), '.vcpkg')));
+ }
+
+ get force() {
+ return !!this.switches['force'];
+ }
+
+ get debug() {
+ return !!this.switches['debug'];
+ }
+
+ get fromVCPKG() {
+ return !!this.switches['from-vcpkg'];
+ }
+
+ get language() {
+ const l = this.switches['language'] || [];
+ strict.ok((l?.length || 0) < 2, i`Expected a single value for ${cmdSwitch('language')} - found multiple`);
+ return l[0] || Intl.DateTimeFormat().resolvedOptions().locale;
+ }
+
+ get allLanguages(): boolean {
+ const l = this.switches['all-languages'] || [];
+ strict.ok((l?.length || 0) < 2, i`Expected a single value for ${cmdSwitch('all-languages')} - found multiple`);
+ return !!l[0];
+ }
+
+ isSet(sw: string) {
+ const s = this.switches[sw];
+ if (s && s.last !== 'false') {
+ return true;
+ }
+ return false;
+ }
+
+ claim(sw: string) {
+ const v = this.switches[sw];
+ delete this.switches[sw];
+ return v;
+ }
+
+ addCommand(command: Command) {
+ this.commands.push(command);
+ }
+
+ /** parses the command line and returns the command that has been requested */
+ get command() {
+ return this.commands.find(cmd => cmd.command === this.inputs[0] || !!cmd.aliases.find(alias => alias === this.inputs[0]));
+ }
+
+ constructor(args: Array) {
+ for (let i = 0; i < args.length; i++) {
+ const arg = args[i];
+ // eslint-disable-next-line prefer-const
+ let [, name, sep, value] = /^--([^=:]+)([=:])?(.+)?$/g.exec(arg) || [];
+ if (name) {
+ if (!value) {
+ if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
+ // if you say --foo bar then bar is the value
+ value = args[++i];
+ }
+ }
+ this.switches[name] = this.switches[name] === undefined ? [] : this.switches[name];
+ this.switches[name].push(value);
+ continue;
+ }
+ this.inputs.push(arg);
+ }
+
+ this.context = intersect(new Ctx(this), this.switches);
+ }
+}
diff --git a/ce/ce/cli/command.ts b/ce/ce/cli/command.ts
new file mode 100644
index 0000000000..80b01505f4
--- /dev/null
+++ b/ce/ce/cli/command.ts
@@ -0,0 +1,80 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { i } from '../i18n';
+import { Argument } from './argument';
+import { CommandLine, Help } from './command-line';
+import { blank, cli } from './constants';
+import { cmdSwitch, command, heading, optional } from './format';
+import { Switch } from './switch';
+import { Debug } from './switches/debug';
+import { Force } from './switches/force';
+
+/** @internal */
+
+export abstract class Command implements Help {
+ readonly abstract command: string;
+ readonly abstract argumentsHelp: Array;
+
+ readonly switches = new Array();
+ readonly arguments = new Array();
+
+ readonly abstract seeAlso: Array;
+ readonly abstract aliases: Array;
+
+ abstract get summary(): string;
+ abstract get description(): Array;
+
+ readonly force = new Force(this);
+ readonly debug = new Debug(this);
+
+ get synopsis(): Array {
+ return [
+ heading(i`Synopsis`, 2),
+ ` ${command(`${cli} ${this.command} ${this.arguments.map(each => `<${each.argument}>`).join(' ')}`)}${this.switches.flatMap(each => optional(`[--${each.switch}]`)).join(' ')}`,
+ ];
+ }
+
+ get title() {
+ return `${cli} ${this.command}`;
+ }
+
+ constructor(public commandLine: CommandLine) {
+ commandLine.addCommand(this);
+ }
+
+ get inputs() {
+ return this.commandLine.inputs.slice(1);
+ }
+
+ get help() {
+ return [
+ heading(this.title),
+ blank,
+ this.summary,
+ blank,
+ ...this.synopsis,
+ blank,
+ heading(i`Description`, 2),
+ blank,
+ ...this.description,
+ ...this.argumentsHelp,
+ ...(this.switches.length ? [
+ blank,
+ heading(i`Switches`, 2),
+ blank,
+ ...this.switches.flatMap(each => ` ${cmdSwitch(each.switch)}: ${each.help.join(' ')}`)
+ ] : []),
+ ...(this.seeAlso.length ? [
+ heading(i`See Also`, 2),
+ ...this.seeAlso.flatMap(each => each.title)
+ ] : []),
+ ];
+ }
+
+ async run() {
+ // do something
+ return true;
+ }
+
+}
diff --git a/ce/ce/cli/commands/acquire.ts b/ce/ce/cli/commands/acquire.ts
new file mode 100644
index 0000000000..b0fdbfb30c
--- /dev/null
+++ b/ce/ce/cli/commands/acquire.ts
@@ -0,0 +1,84 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { i } from '../../i18n';
+import { session } from '../../main';
+import { countWhere } from '../../util/linq';
+import { installArtifacts, selectArtifacts, showArtifacts } from '../artifacts';
+import { Command } from '../command';
+import { blank } from '../constants';
+import { cmdSwitch } from '../format';
+import { debug, error, log, warning } from '../styling';
+import { Registry } from '../switches/registry';
+import { Version } from '../switches/version';
+import { WhatIf } from '../switches/whatIf';
+
+export class AcquireCommand extends Command {
+ readonly command = 'acquire';
+ readonly aliases = ['install'];
+ seeAlso = [];
+ argumentsHelp = [];
+ version = new Version(this);
+ whatIf = new WhatIf(this);
+ registrySwitch = new Registry(this);
+
+ get summary() {
+ return i`Acquire artifacts in the registry`;
+ }
+
+ get description() {
+ return [
+ i`This allows the consumer to acquire (download and unpack) artifacts. Artifacts must be activated to be used`,
+ ];
+ }
+
+ override async run() {
+ if (this.inputs.length === 0) {
+ error(i`No artifacts specified`);
+ return false;
+ }
+
+ const registries = await this.registrySwitch.loadRegistries(session);
+
+ const versions = this.version.values;
+ if (versions.length && this.inputs.length !== versions.length) {
+ error(i`Multiple packages specified, but not an equal number of ${cmdSwitch('version')} switches.`);
+ return false;
+ }
+
+ const artifacts = await selectArtifacts(new Map(this.inputs.map((v, i) => [v, versions[i] || '*'])), registries);
+
+ if (!artifacts) {
+ debug('No artifacts selected - stopping');
+ return false;
+ }
+
+ if (!await showArtifacts(artifacts.artifacts, this.commandLine)) {
+ warning(i`No artifacts are acquired`);
+ return false;
+ }
+
+ const numberOfArtifacts = await countWhere(artifacts.artifacts, async (artifact) => !(!this.commandLine.force && await artifact.isInstalled));
+
+ if (!numberOfArtifacts) {
+ log(blank);
+ log(i`All artifacts are already installed`);
+ return true;
+ }
+
+ debug(`Installing ${numberOfArtifacts} artifacts`);
+
+ const [success] = await installArtifacts(session, artifacts.artifacts, { force: this.commandLine.force, language: this.commandLine.language, allLanguages: this.commandLine.allLanguages });
+
+ if (success) {
+ log(blank);
+ log(i`${numberOfArtifacts} artifacts installed successfuly`);
+ return true;
+ }
+
+ log(blank);
+ log(i`Installation failed -- stopping`);
+
+ return false;
+ }
+}
diff --git a/ce/ce/cli/commands/activate.ts b/ce/ce/cli/commands/activate.ts
new file mode 100644
index 0000000000..df2d8f7097
--- /dev/null
+++ b/ce/ce/cli/commands/activate.ts
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { i } from '../../i18n';
+import { session } from '../../main';
+import { Command } from '../command';
+import { projectFile } from '../format';
+import { activateProject } from '../project';
+import { debug, error } from '../styling';
+import { MSBuildProps } from '../switches/msbuild-props';
+import { Project } from '../switches/project';
+import { WhatIf } from '../switches/whatIf';
+
+export class ActivateCommand extends Command {
+ readonly command = 'activate';
+ readonly aliases = [];
+ seeAlso = [];
+ argumentsHelp = [];
+ whatIf = new WhatIf(this);
+ project: Project = new Project(this);
+ msbuildProps: MSBuildProps = new MSBuildProps(this);
+
+ get summary() {
+ return i`Activates the tools required for a project`;
+ }
+
+ get description() {
+ return [
+ i`This allows the consumer to Activate the tools required for a project. If the tools are not already installed, this will force them to be downloaded and installed before activation.`,
+ ];
+ }
+
+ override async run() {
+ const projectManifest = await this.project.manifest;
+
+ if (!projectManifest) {
+ error(i`Unable to find project in folder (or parent folders) for ${session.currentDirectory.fsPath}`);
+ return false;
+ }
+
+ debug(i`Deactivating project ${projectFile(projectManifest.metadata.context.file)}`);
+ await session.deactivate();
+
+ return await activateProject(projectManifest, {
+ force: this.commandLine.force,
+ allLanguages: this.commandLine.allLanguages,
+ language: this.commandLine.language,
+ msbuildProps: await this.msbuildProps.value
+ });
+ }
+}
diff --git a/ce/ce/cli/commands/add.ts b/ce/ce/cli/commands/add.ts
new file mode 100644
index 0000000000..986c12d706
--- /dev/null
+++ b/ce/ce/cli/commands/add.ts
@@ -0,0 +1,90 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { i } from '../../i18n';
+import { session } from '../../main';
+import { selectArtifacts } from '../artifacts';
+import { Command } from '../command';
+import { cmdSwitch, projectFile } from '../format';
+import { activateProject } from '../project';
+import { debug, error } from '../styling';
+import { Project } from '../switches/project';
+import { Registry } from '../switches/registry';
+import { Version } from '../switches/version';
+import { WhatIf } from '../switches/whatIf';
+
+export class AddCommand extends Command {
+ readonly command = 'add';
+ readonly aliases = [];
+ seeAlso = [];
+ argumentsHelp = [];
+
+ version = new Version(this);
+ project: Project = new Project(this);
+ whatIf = new WhatIf(this);
+ registrySwitch = new Registry(this);
+
+
+ get summary() {
+ return i`Adds an artifact to the project`;
+ }
+
+ get description() {
+ return [
+ i`This allows the consumer to add an artifact to the project. This will activate the project as well.`,
+ ];
+ }
+
+ override async run() {
+ const projectManifest = await this.project.manifest;
+
+ if (!projectManifest) {
+ error(i`Unable to find project in folder (or parent folders) for ${session.currentDirectory.fsPath}`);
+ return false;
+ }
+
+ // pull in any registries that are on the command line
+ await this.registrySwitch.loadRegistries(session);
+
+ if (this.inputs.length === 0) {
+ error(i`No artifacts specified`);
+ return false;
+ }
+
+ const versions = this.version.values;
+ if (versions.length && this.inputs.length !== versions.length) {
+ error(i`Multiple artifacts specified, but not an equal number of ${cmdSwitch('version')} switches`);
+ return false;
+ }
+
+ const selections = new Map(this.inputs.map((v, i) => [v, versions[i] || '*']));
+ const selectedArtifacts = await selectArtifacts(selections, projectManifest.registries);
+
+ if (!selectedArtifacts) {
+ return false;
+ }
+
+ for (const [artifact, id, requested] of selectedArtifacts.values()) {
+ // make sure the registry is in the project
+ const registry = projectManifest.registries.getRegistry(artifact.registryUri);
+ if (!registry) {
+ const r = projectManifest.metadata.registries.add(artifact.registryId, artifact.registryUri, 'artifact');
+
+ }
+
+
+ // add the artifact to the project
+ const fulfilled = artifact.version.toString();
+ const v = requested !== fulfilled ? `${requested} ${fulfilled}` : fulfilled;
+ projectManifest.metadata.requires.set(artifact.reference, v);
+ }
+
+ // write the file out.
+ await projectManifest.metadata.save();
+
+ debug(i`Deactivating project ${projectFile(projectManifest.metadata.context.file)}`);
+ await session.deactivate();
+
+ return await activateProject(projectManifest, this.commandLine);
+ }
+}
\ No newline at end of file
diff --git a/ce/ce/cli/commands/apply-vsman.ts b/ce/ce/cli/commands/apply-vsman.ts
new file mode 100644
index 0000000000..d544d208e2
--- /dev/null
+++ b/ce/ce/cli/commands/apply-vsman.ts
@@ -0,0 +1,139 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { MetadataFile } from '../../amf/metadata-file';
+import { acquireArtifactFile } from '../../fs/acquire';
+import { FileType } from '../../fs/filesystem';
+import { i } from '../../i18n';
+import { session } from '../../main';
+import { Session } from '../../session';
+import { Uri } from '../../util/uri';
+import { templateAmfApplyVsManifestInformation } from '../../willow/template-amf';
+import { parseVsManFromChannel, VsManDatabase } from '../../willow/willow';
+import { Command } from '../command';
+import { log } from '../styling';
+import { Switch } from '../switch';
+
+class ChannelUri extends Switch {
+ readonly switch = 'channel';
+
+ get help() {
+ return [
+ i`The URI to the Visual Studio channel to apply.`
+ ];
+ }
+}
+
+class RepoRoot extends Switch {
+ readonly switch = 'repo';
+
+ get help() {
+ return [
+ i`The directory path to the root of the repo into which artifact metadata is to be generated.`
+ ];
+ }
+}
+
+export class ApplyVsManCommand extends Command {
+ readonly command = 'z-apply-vsman';
+ readonly seeAlso = [];
+ readonly argumentsHelp = [];
+ readonly aliases = [];
+
+ readonly channelUri = new ChannelUri(this);
+ readonly repoRoot = new RepoRoot(this);
+
+ get summary() {
+ return i`Apply Visual Studio Channel (.vsman) information to a prototypical artifact metadata.`;
+ }
+
+ get description() {
+ return [
+ i`This is used to mint artifacts metadata exactly corresponding to a release state in a Visual Studio channel.`,
+ ];
+ }
+
+ /**
+ * Process an input file.
+ */
+ static async processFile(session: Session, inputUri: Uri, repoRoot: Uri, vsManLookup: VsManDatabase) {
+ const inputPath = inputUri.fsPath;
+ session.channels.debug(i`Processing ${inputPath}...`);
+ const inputContent = await inputUri.readUTF8();
+ const outputContent = templateAmfApplyVsManifestInformation(session, inputPath, inputContent, vsManLookup);
+ if (!outputContent) {
+ session.channels.warning(i`Skipped processing ${inputPath}`);
+ return 0;
+ }
+
+ const outputAmf = await MetadataFile.parseConfiguration(inputPath, outputContent, session);
+ if (!outputAmf.isValid) {
+ const errors = outputAmf.validationErrors.join('\n');
+ session.channels.warning(i`After transformation, ${inputPath} did not result in a valid AMF; skipping:\n${outputContent}\n${errors}`);
+ return 0;
+ }
+
+ const outputId = outputAmf.info.id;
+ const outputIdLast = outputId.slice(outputId.lastIndexOf('/'));
+ const outputVersion = outputAmf.info.version;
+ const outputRelativePath = `${outputId}/${outputIdLast}-${outputVersion}.yaml`;
+ const outputFullPath = repoRoot.join(outputRelativePath);
+ let doWrite = true;
+ try {
+ const outputExistingContent = await outputFullPath.readUTF8();
+ if (outputExistingContent === outputContent) {
+ doWrite = false;
+ } else {
+ session.channels.warning(i`After transformation, ${inputPath} results in ${outputFullPath.toString()} which already exists; overwriting.`);
+ }
+ } catch {
+ // nothing to do
+ }
+
+ if (doWrite) {
+ await outputFullPath.writeUTF8(outputContent);
+ }
+
+ session.channels.debug(i`-> ${outputFullPath.toString()}`);
+ return 1;
+ }
+
+ /**
+ * Process an input file or directory, recursively.
+ */
+ static async processInput(session: Session, inputDirectoryEntry: [Uri, FileType], repoRoot: Uri, vsManLookup: VsManDatabase): Promise {
+ if ((inputDirectoryEntry[1] & FileType.Directory) !== 0) {
+ let total = 0;
+ for (const child of await inputDirectoryEntry[0].readDirectory()) {
+ total += await ApplyVsManCommand.processInput(session, child, repoRoot, vsManLookup);
+ }
+
+ return total;
+ } else if ((inputDirectoryEntry[1] & FileType.File) !== 0) {
+ return await ApplyVsManCommand.processFile(session, inputDirectoryEntry[0], repoRoot, vsManLookup);
+ }
+
+ return 0;
+ }
+
+ override async run() {
+ const channelUriStr = this.channelUri.requiredValue;
+ const repoRoot = session.fileSystem.file(this.repoRoot.requiredValue);
+ log(i`Downloading channel manifest from ${channelUriStr}`);
+ const channelUriUri = session.parseUri(channelUriStr);
+ const channelFile = await acquireArtifactFile(session, [channelUriUri], 'channel.chman', {});
+ const vsManPayload = parseVsManFromChannel(await channelFile.readUTF8());
+ log(i`Downloading Visual Studio manifest version ${vsManPayload.version} (${vsManPayload.url})`);
+ const vsManUri = await acquireArtifactFile(session, [session.parseUri(vsManPayload.url)], vsManPayload.fileName, {});
+ const vsManLookup = new VsManDatabase(await vsManUri.readUTF8());
+ let totalProcessed = 0;
+ for (const inputPath of this.inputs) {
+ const inputUri = session.fileSystem.file(inputPath);
+ const inputStat = await inputUri.stat();
+ totalProcessed += await ApplyVsManCommand.processInput(session, [inputUri, inputStat.type], repoRoot, vsManLookup);
+ }
+
+ session.channels.message(i`Processed ${totalProcessed} templates.`);
+ return true;
+ }
+}
diff --git a/ce/ce/cli/commands/cache.ts b/ce/ce/cli/commands/cache.ts
new file mode 100644
index 0000000000..980d937293
--- /dev/null
+++ b/ce/ce/cli/commands/cache.ts
@@ -0,0 +1,62 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { basename } from 'path';
+import { FileType } from '../../fs/filesystem';
+import { i } from '../../i18n';
+import { session } from '../../main';
+import { Uri } from '../../util/uri';
+import { Command } from '../command';
+import { Table } from '../markdown-table';
+import { log } from '../styling';
+import { Clear } from '../switches/clear';
+import { WhatIf } from '../switches/whatIf';
+
+export class CacheCommand extends Command {
+ readonly command = 'cache';
+ readonly aliases = [];
+ seeAlso = [];
+ argumentsHelp = [];
+ clear = new Clear(this);
+ whatIf = new WhatIf(this);
+
+ get summary() {
+ return i`Manages the download cache`;
+ }
+
+ get description() {
+ return [
+ i`Manages the download cache.`,
+ ];
+ }
+
+ override async run() {
+ if (this.clear.active) {
+ await session.cache.delete({ recursive: true });
+ await session.cache.createDirectory();
+ log(i`Cache folder cleared (${session.cache.fsPath}) `);
+ return true;
+ }
+ let files: Array<[Uri, FileType]> = [];
+ try {
+ files = await session.cache.readDirectory();
+ } catch {
+ // shh
+ }
+
+ if (!files.length) {
+ log('The download cache is empty');
+ return true;
+ }
+
+ const table = new Table('File', 'Size', 'Date');
+ for (const [file, type] of files) {
+ const stat = await file.stat();
+ table.push(basename(file.fsPath), stat.size.toString(), new Date(stat.mtime).toString());
+ }
+ log(table.toString());
+ log();
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/ce/ce/cli/commands/clean.ts b/ce/ce/cli/commands/clean.ts
new file mode 100644
index 0000000000..b8360121af
--- /dev/null
+++ b/ce/ce/cli/commands/clean.ts
@@ -0,0 +1,78 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { i } from '../../i18n';
+import { session } from '../../main';
+import { Command } from '../command';
+import { debug, log } from '../styling';
+import { Switch } from '../switch';
+import { WhatIf } from '../switches/whatIf';
+
+export class All extends Switch {
+ switch = 'all';
+ get help() {
+ return [
+ i`cleans out everything (cache, installed artifacts)`
+ ];
+ }
+}
+
+export class Cache extends Switch {
+ switch = 'cache';
+ get help() {
+ return [
+ i`cleans out the cache`
+ ];
+ }
+}
+
+export class Artifacts extends Switch {
+ switch = 'artifacts';
+ get help() {
+ return [
+ i`removes all the artifacts that are installed`
+ ];
+ }
+}
+
+export class CleanCommand extends Command {
+ readonly command = 'clean';
+ readonly aliases = [];
+ seeAlso = [];
+ argumentsHelp = [];
+ all = new All(this);
+ artifacts = new Artifacts(this);
+ cache = new Cache(this);
+ whatIf = new WhatIf(this);
+
+ get summary() {
+ return i`cleans up`;
+ }
+
+ get description() {
+ return [
+ i`Allows the user to clean out the cache, installed artifacts, etc.`,
+ ];
+ }
+
+ override async run() {
+
+ if (this.all.active || this.artifacts.active) {
+ // if we're removing artifacts
+ debug(i`Deactivating project`);
+ await session.deactivate();
+
+ await session.installFolder.delete({ recursive: true });
+ await session.installFolder.createDirectory();
+ log(i`Installed Artifact folder cleared (${session.installFolder.fsPath}) `);
+ }
+
+ if (this.all.active || this.cache.active) {
+ await session.cache.delete({ recursive: true });
+ await session.cache.createDirectory();
+ log(i`Cache folder cleared (${session.cache.fsPath}) `);
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/ce/ce/cli/commands/deactivate.ts b/ce/ce/cli/commands/deactivate.ts
new file mode 100644
index 0000000000..d6fc8e94c5
--- /dev/null
+++ b/ce/ce/cli/commands/deactivate.ts
@@ -0,0 +1,41 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { i } from '../../i18n';
+import { session } from '../../main';
+import { Command } from '../command';
+import { projectFile } from '../format';
+import { log } from '../styling';
+import { Project } from '../switches/project';
+import { WhatIf } from '../switches/whatIf';
+
+export class DeactivateCommand extends Command {
+ readonly command = 'deactivate';
+ readonly aliases = [];
+ seeAlso = [];
+ argumentsHelp = [];
+ project = new Project(this);
+ whatIf = new WhatIf(this);
+
+ get summary() {
+ return i`Deactivates the current session`;
+ }
+
+ get description() {
+ return [
+ i`This allows the consumer to remove environment settings for the currently active session.`,
+ ];
+ }
+
+ override async run() {
+ const project = await this.project.value;
+ if (!project) {
+ return false;
+ }
+
+ log(i`Deactivating project ${projectFile(project)}`);
+ await session.deactivate();
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/ce/ce/cli/commands/delete.ts b/ce/ce/cli/commands/delete.ts
new file mode 100644
index 0000000000..68c23e2cfb
--- /dev/null
+++ b/ce/ce/cli/commands/delete.ts
@@ -0,0 +1,42 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { i } from '../../i18n';
+import { session } from '../../main';
+import { Command } from '../command';
+import { Version } from '../switches/version';
+import { WhatIf } from '../switches/whatIf';
+
+export class DeleteCommand extends Command {
+ readonly command = 'delete';
+ readonly aliases = ['uninstall'];
+ seeAlso = [];
+ argumentsHelp = [];
+ version = new Version(this);
+ whatIf = new WhatIf(this);
+
+ get summary() {
+ return i`Deletes an artifact from the artifact folder`;
+ }
+
+ get description() {
+ return [
+ i`This allows the consumer to remove an artifact from disk.`,
+ ];
+ }
+
+ override async run() {
+ const artifacts = await session.getInstalledArtifacts();
+ for (const input of this.inputs) {
+ for (const { artifact, id, folder } of artifacts) {
+ if (input === id) {
+ if (await folder.exists()) {
+ session.channels.message(i`Deleting artifact ${id} from ${folder.fsPath}`);
+ await artifact.uninstall();
+ }
+ }
+ }
+ }
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/ce/ce/cli/commands/find.ts b/ce/ce/cli/commands/find.ts
new file mode 100644
index 0000000000..5f1b42f73e
--- /dev/null
+++ b/ce/ce/cli/commands/find.ts
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+
+import { i } from '../../i18n';
+import { session } from '../../main';
+import { Registries } from '../../registries/registries';
+import { Command } from '../command';
+import { artifactIdentity } from '../format';
+import { Table } from '../markdown-table';
+import { debug, log } from '../styling';
+import { Project } from '../switches/project';
+import { Registry } from '../switches/registry';
+import { Version } from '../switches/version';
+
+export class FindCommand extends Command {
+ readonly command = 'find';
+ readonly aliases = ['search'];
+ seeAlso = [];
+ argumentsHelp = [];
+
+ version = new Version(this);
+ registrySwitch = new Registry(this);
+ project = new Project(this);
+
+ get summary() {
+ return i`Find artifacts in the registry`;
+ }
+
+ get description() {
+ return [
+ i`This allows the user to find artifacts based on some criteria.`,
+ ];
+ }
+
+ override async run() {
+ // load registries (from the current project too if available)
+ let registries: Registries = await this.registrySwitch.loadRegistries(session);
+ registries = (await this.project.manifest)?.registries ?? registries;
+
+ debug(`using registries: ${[...registries].map(([registry, registryNames]) => registryNames[0]).join(', ')}`);
+ const table = new Table('Artifact', 'Version', 'Summary');
+
+ for (const each of this.inputs) {
+ for (const [registry, id, artifacts] of await registries.search({ idOrShortName: each, version: this.version.value })) {
+ const latest = artifacts[0];
+ if (!latest.metadata.info.dependencyOnly) {
+ const name = artifactIdentity(latest.registryId, id, latest.shortName);
+ table.push(name, latest.metadata.info.version, latest.metadata.info.summary || '');
+ }
+ }
+ }
+
+ log(table.toString());
+ log();
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/ce/ce/cli/commands/help.ts b/ce/ce/cli/commands/help.ts
new file mode 100644
index 0000000000..3380a8a13e
--- /dev/null
+++ b/ce/ce/cli/commands/help.ts
@@ -0,0 +1,84 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { i } from '../../i18n';
+import { Argument } from '../argument';
+import { Command } from '../command';
+import { blank, cli } from '../constants';
+import { command as formatCommand, heading, hint } from '../format';
+import { error, indent, log } from '../styling';
+
+class CommandName extends Argument {
+ argument = 'command';
+
+ get help() {
+ return [
+ i`the name of the command for which you want help`
+ ];
+ }
+}
+
+/**@internal */
+export class HelpCommand extends Command {
+ readonly command = 'help';
+ readonly aliases = [];
+ seeAlso = [];
+ commandName: CommandName = new CommandName(this);
+
+ get argumentsHelp() {
+ return [indent(i` <${this.commandName.argument}> : ${this.commandName.help.join(' ')}`)];
+ }
+
+ get summary() {
+ return i`get help on ${cli} or one of the commands`;
+ }
+
+ get description() {
+ return [
+ i`Gets detailed help on ${cli}, or one of the commands`,
+ blank,
+ i`Arguments:`
+ ];
+ }
+
+ override async run() {
+ const cmd = ['-h', '-help', '-?', '/?'].find(each => (this.commandLine.inputs.indexOf(each) > -1)) ? this.commandLine.inputs[0] : this.commandLine.inputs[1];
+ // did they ask for help on a command?
+
+ if (cmd) {
+ const target = this.commandLine.commands.find(each => each.command === cmd);
+ if (target) {
+ log(target.help.join('\n'));
+ log(blank);
+ return true;
+ }
+
+ // I don't know the command
+ error(i`Unrecognized command '${cmd}'`);
+ log(hint(i`Use ${formatCommand(`${cli} ${this.command}`)} to get the list of available commands`));
+ return false;
+ }
+
+ // general help. return the general help info
+
+ log(heading(i`Usage`, 2));
+ log(blank);
+ log(indent(i`${cli} COMMAND [--switches]`));
+ log(blank);
+
+ log(heading(i`Available ${cli} commands:`, 2));
+ log(blank);
+ const max = Math.max(...this.commandLine.commands.map(each => each.command.length));
+ for (const command of this.commandLine.commands) {
+ if (command.command.startsWith('z-')) {
+ // don't show internal commands
+ continue;
+ }
+ log(indent(i`${formatCommand(command.command.padEnd(max))} : ${command.summary}`));
+ }
+
+ log(blank);
+
+ return true;
+ }
+}
diff --git a/ce/ce/cli/commands/list.ts b/ce/ce/cli/commands/list.ts
new file mode 100644
index 0000000000..0838b06dd6
--- /dev/null
+++ b/ce/ce/cli/commands/list.ts
@@ -0,0 +1,48 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { i } from '../../i18n';
+import { session } from '../../main';
+import { Command } from '../command';
+import { artifactIdentity } from '../format';
+import { Table } from '../markdown-table';
+import { log } from '../styling';
+import { Installed } from '../switches/installed';
+
+export class ListCommand extends Command {
+ readonly command = 'list';
+ readonly aliases = ['show'];
+ seeAlso = [];
+ argumentsHelp = [];
+ installed = new Installed(this);
+
+ get summary() {
+ return i`Lists the artifacts`;
+ }
+
+ get description() {
+ return [
+ i`This allows the consumer to list artifacts.`,
+ ];
+ }
+
+ override async run() {
+ if (this.installed.active) {
+ const artifacts = await session.getInstalledArtifacts();
+ const table = new Table('Artifact', 'Version', 'Summary');
+
+ for (const { artifact, id, folder } of artifacts) {
+
+ const name = artifactIdentity('', id); //todo: fixme
+ table.push(name, artifact.version, artifact.metadata.info.summary || '');
+ }
+ log(table.toString());
+ log();
+ }
+ else {
+ log('use --installed for now');
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/ce/ce/cli/commands/new.ts b/ce/ce/cli/commands/new.ts
new file mode 100644
index 0000000000..21c6857e25
--- /dev/null
+++ b/ce/ce/cli/commands/new.ts
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { MetadataFile } from '../../amf/metadata-file';
+import { i } from '../../i18n';
+import { session } from '../../main';
+import { Command } from '../command';
+import { project } from '../constants';
+import { log } from '../styling';
+import { WhatIf } from '../switches/whatIf';
+
+export class NewCommand extends Command {
+ readonly command = 'new';
+ readonly aliases = [];
+ seeAlso = [];
+ argumentsHelp = [];
+ whatIf = new WhatIf(this);
+
+ get summary() {
+ return i`Creates a new project file`;
+ }
+
+ get description() {
+ return [
+ i`This allows the consumer create a new project file ('${project}').`,
+ ];
+ }
+
+ override async run() {
+ if (await session.currentDirectory.exists(project)) {
+ log(i`The folder at ${session.currentDirectory.fsPath} already contains a project file '${project}'`);
+ return false;
+ }
+ const prjFile = session.currentDirectory.join(project);
+
+ await (await MetadataFile.parseConfiguration(prjFile.toString(), '# Environment configuration\n', session)).save(session.currentDirectory.join(project));
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/ce/ce/cli/commands/regenerate-index.ts b/ce/ce/cli/commands/regenerate-index.ts
new file mode 100644
index 0000000000..3afeb8399c
--- /dev/null
+++ b/ce/ce/cli/commands/regenerate-index.ts
@@ -0,0 +1,82 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { Registry } from '../../artifacts/registry';
+import { registryIndexFile } from '../../constants';
+import { i } from '../../i18n';
+import { session } from '../../main';
+import { Registries } from '../../registries/registries';
+import { Uri } from '../../util/uri';
+import { Command } from '../command';
+import { cli } from '../constants';
+import { error, log, writeException } from '../styling';
+import { Project } from '../switches/project';
+import { Registry as RegSwitch } from '../switches/registry';
+import { WhatIf } from '../switches/whatIf';
+
+export class RegenerateCommand extends Command {
+ readonly command = 'regenerate';
+ project = new Project(this);
+ readonly aliases = ['regen'];
+ readonly regSwitch = new RegSwitch(this, { required: true });
+ seeAlso = [];
+ argumentsHelp = [];
+
+ whatIf = new WhatIf(this);
+ get summary() {
+ return i`regenerate the index for a registry`;
+ }
+
+ get description() {
+ return [
+ i`This allows the user to regenerate the ${registryIndexFile} files for a ${cli} registry.`,
+ ];
+ }
+
+ override async run() {
+ let registries: Registries = await this.regSwitch.loadRegistries(session);
+ registries = (await this.project.manifest)?.registries ?? registries;
+
+ for (const registryNameOrLocation of this.inputs) {
+ let registry: Registry | undefined;
+ try {
+ if (registries.has(registryNameOrLocation)) {
+ // check for named registries first.
+ registry = registries.getRegistry(registryNameOrLocation);
+ await registry?.load();
+ } else {
+ // see if the name is a location
+ const location = await session.parseLocation(registryNameOrLocation);
+ registry = location ?
+ session.loadRegistry(location, 'artifact') : // a folder
+ registries.getRegistry(registryNameOrLocation); // a registry name or other location.
+ }
+ if (registry) {
+ if (Uri.isInvalid(registry.location)) {
+ error(i`Registry: '${registryNameOrLocation}' does not have an index to regenerate.`);
+ return false;
+ }
+ log(i`Regenerating index for ${registry.location.formatted}`);
+ await registry.regenerate();
+ if (registry.count) {
+ await registry.save();
+ log(i`Regeneration complete. Index contains ${registry.count} metadata files`);
+ continue;
+ }
+ // looks like the registry contained no items
+ error(i`Registry: '${registry.location.formatted}' contains no artifacts.`);
+ continue;
+ }
+
+ error(i`Unrecognized registry: ${registryNameOrLocation}`);
+ return false;
+
+ } catch (e) {
+ log(i`Regeneration failed for ${registryNameOrLocation.toString()}`);
+ writeException(e);
+ return false;
+ }
+ }
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/ce/ce/cli/commands/remove.ts b/ce/ce/cli/commands/remove.ts
new file mode 100644
index 0000000000..328f41b2c9
--- /dev/null
+++ b/ce/ce/cli/commands/remove.ts
@@ -0,0 +1,64 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { i } from '../../i18n';
+import { session } from '../../main';
+import { Command } from '../command';
+import { projectFile } from '../format';
+import { activateProject } from '../project';
+import { debug, error, log } from '../styling';
+import { Project } from '../switches/project';
+import { WhatIf } from '../switches/whatIf';
+
+export class RemoveCommand extends Command {
+ readonly command = 'remove';
+ readonly aliases = [];
+ seeAlso = [];
+ argumentsHelp = [];
+ whatIf = new WhatIf(this);
+ project: Project = new Project(this);
+
+ get summary() {
+ return i`Removes an artifact from a project`;
+ }
+
+ get description() {
+ return [
+ i`This allows the consumer to remove an artifact from the project. Forces reactivation in this window.`,
+ ];
+ }
+
+ override async run() {
+ const projectManifest = await this.project.manifest;
+
+ if (!projectManifest) {
+ error(i`Unable to find project in folder (or parent folders) for ${session.currentDirectory.fsPath}`);
+ return false;
+ }
+
+ if (this.inputs.length === 0) {
+ error(i`No artifacts specified`);
+ return false;
+ }
+
+
+ const req = projectManifest.metadata.requires.keys;
+ for (const input of this.inputs) {
+ if (req.indexOf(input) !== -1) {
+ projectManifest.metadata.requires.delete(input);
+ log(i`Removing ${input} from project manifest`);
+ } else {
+ error(i`unable to find artifact ${input} in the project manifest`);
+ return false;
+ }
+ }
+
+ // write the file out.
+ await projectManifest.metadata.save();
+
+ debug(`Deactivating project ${projectFile(projectManifest.metadata.context.file)}`);
+ await session.deactivate();
+
+ return await activateProject(projectManifest, this.commandLine);
+ }
+}
\ No newline at end of file
diff --git a/ce/ce/cli/commands/update.ts b/ce/ce/cli/commands/update.ts
new file mode 100644
index 0000000000..8ce7f4ba34
--- /dev/null
+++ b/ce/ce/cli/commands/update.ts
@@ -0,0 +1,91 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+
+import { Registry } from '../../artifacts/registry';
+import { i } from '../../i18n';
+import { session } from '../../main';
+import { RemoteFileUnavailable } from '../../util/exceptions';
+import { Command } from '../command';
+import { CommandLine } from '../command-line';
+import { count } from '../format';
+import { error, log, writeException } from '../styling';
+import { Registry as RegSwitch } from '../switches/registry';
+import { WhatIf } from '../switches/whatIf';
+
+export class UpdateCommand extends Command {
+ readonly command = 'update';
+ readonly aliases = [];
+ seeAlso = [];
+ argumentsHelp = [];
+ whatIf = new WhatIf(this);
+ registrySwitch = new RegSwitch(this);
+
+ get summary() {
+ return i`update the registry from the remote`;
+ }
+
+ get description() {
+ return [
+ i`This downloads the latest contents of the registry from the remote service.`,
+ ];
+ }
+
+ override async run() {
+ const registries = await this.registrySwitch.loadRegistries(session);
+
+ // process named registries
+ for (let registryName of this.inputs) {
+ if (registryName.indexOf(':') !== -1) {
+ registryName = session.parseUri(registryName).toString();
+ }
+ const registry = registries.getRegistryWithNameOrLocation(registryName);
+ if (registry) {
+ try {
+ log(i`Downloading registry data`);
+ await registry.update();
+ await registry.load();
+ log(i`Updated ${registryName}. registry contains ${count(registry.count)} metadata files`);
+ } catch (e) {
+ if (e instanceof RemoteFileUnavailable) {
+ log(i`Unable to download registry snapshot`);
+ return false;
+ }
+ writeException(e);
+ return false;
+ }
+ } else {
+ error(i`Unable to find registry ${registryName}`);
+ }
+ }
+
+ return true;
+ }
+
+ static async update(registry: Registry) {
+ log(i`Artifact registry data is not loaded`);
+ log(i`Attempting to update artifact registry`);
+ const update = new UpdateCommand(new CommandLine([]));
+
+ let success = true;
+ try {
+ success = await update.run();
+ } catch (e) {
+ writeException(e);
+ success = false;
+ }
+ if (!success) {
+ error(i`Unable to load registry index`);
+ return false;
+ }
+ try {
+ await registry.load();
+ } catch (e) {
+ writeException(e);
+ // it just doesn't want to load.
+ error(i`Unable to load registry index`);
+ return false;
+ }
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/ce/ce/cli/commands/use.ts b/ce/ce/cli/commands/use.ts
new file mode 100644
index 0000000000..4b8bdad03b
--- /dev/null
+++ b/ce/ce/cli/commands/use.ts
@@ -0,0 +1,79 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { i } from '../../i18n';
+import { session } from '../../main';
+import { Registries } from '../../registries/registries';
+import { installArtifacts, selectArtifacts, showArtifacts } from '../artifacts';
+import { Command } from '../command';
+import { cmdSwitch } from '../format';
+import { error, log, warning } from '../styling';
+import { MSBuildProps } from '../switches/msbuild-props';
+import { Project } from '../switches/project';
+import { Registry } from '../switches/registry';
+import { Version } from '../switches/version';
+import { WhatIf } from '../switches/whatIf';
+
+export class UseCommand extends Command {
+ readonly command = 'use';
+ readonly aliases = [];
+ seeAlso = [];
+ argumentsHelp = [];
+ version = new Version(this);
+ whatIf = new WhatIf(this);
+ registrySwitch = new Registry(this);
+ project = new Project(this);
+ msbuildProps = new MSBuildProps(this);
+
+ get summary() {
+ return i`Instantly activates an artifact outside of the project`;
+ }
+
+ get description() {
+ return [
+ i`This will instantly activate an artifact .`,
+ ];
+ }
+
+ override async run() {
+ if (this.inputs.length === 0) {
+ error(i`No artifacts specified`);
+ return false;
+ }
+
+ // load registries (from the current project too if available)
+ let registries: Registries = await this.registrySwitch.loadRegistries(session);
+ registries = (await this.project.manifest)?.registries ?? registries;
+
+ const versions = this.version.values;
+ if (versions.length && this.inputs.length !== versions.length) {
+ error(i`Multiple packages specified, but not an equal number of ${cmdSwitch('version')} switches`);
+ return false;
+ }
+
+ const selections = new Map(this.inputs.map((v, i) => [v, versions[i] || '*']));
+ const artifacts = await selectArtifacts(selections, registries);
+
+ if (!artifacts) {
+ return false;
+ }
+
+ if (!await showArtifacts(artifacts.artifacts, this.commandLine)) {
+ warning(i`No artifacts are being acquired`);
+ return false;
+ }
+
+ const [success, artifactStatus, activation] = await installArtifacts(session, artifacts.artifacts, { force: this.commandLine.force, language: this.commandLine.language, allLanguages: this.commandLine.allLanguages });
+ if (success) {
+ log(i`Activating individual artifacts`);
+ await session.setActivationInPostscript(activation, false);
+ const propsFile = this.msbuildProps.value;
+ if (propsFile) {
+ await propsFile.writeUTF8(activation.generateMSBuild(artifactStatus.keys()));
+ }
+ } else {
+ return false;
+ }
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/ce/ce/cli/commands/version.ts b/ce/ce/cli/commands/version.ts
new file mode 100644
index 0000000000..0ad631d797
--- /dev/null
+++ b/ce/ce/cli/commands/version.ts
@@ -0,0 +1,111 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { strict } from 'assert';
+import { parse } from 'semver';
+import { i } from '../../i18n';
+import { session } from '../../main';
+import { Version } from '../../version';
+import { Command } from '../command';
+import { cli, product } from '../constants';
+import { debug, error, log } from '../styling';
+import { Switch } from '../switch';
+
+class Check extends Switch {
+ switch = 'check';
+ get help() {
+ return [
+ i`check to see if a newer version of ${cli} is available`
+ ];
+ }
+}
+
+class Update extends Switch {
+ switch = 'update';
+ get help() {
+ return [
+ i`will update the current installation of ${cli} if a newer version is available`
+ ];
+ }
+}
+
+export class VersionCommand extends Command {
+ readonly command = 'version';
+ readonly aliases = ['ver'];
+ seeAlso = [];
+ argumentsHelp = [];
+ check = new Check(this);
+ update = new Update(this);
+
+ versionUrl = session.parseUri('https://aka.ms/vcpkg-ce.version');
+
+ get summary() {
+ return i`manage the version of ${cli}`;
+ }
+
+ get description() {
+ return [
+ i`This allows the user to get the current verison information for ${cli}`,
+ i`as well as checking if a new version is available, and can upgrade the current installation to the latest version.`,
+ ];
+ }
+
+ private async getRemoteVersion() {
+ const version = await this.versionUrl.readUTF8();
+ const semver = parse(version.trim());
+ strict.ok(semver, i`Unable to parse version ${version}`);
+
+ return semver;
+ }
+
+ override async run() {
+ if (this.update.active) {
+ // check for a new version, and update if necessary
+ debug(i`checking to see if there is a new version of the ${cli}, and updating if there is`);
+ try {
+ const semver = await this.getRemoteVersion();
+
+ if (semver.compare(Version) > 0) {
+ // we can update the tool.
+ debug('An update is available, we can install it. ');
+ debug('(we can not do it yet, waiting for download support');
+ }
+
+ } catch (err) {
+ error('Failed to get latest version number');
+ return false;
+ }
+ return true;
+ }
+
+ if (this.check.active) {
+ // check for a new version
+ debug(i`checking to see if there is a new version of the ${cli}`);
+ try {
+ const semver = await this.getRemoteVersion();
+
+ if (semver.compare(Version) > 0) {
+ log(i`There is a new version (${semver.version}) of ${cli} available`);
+ }
+ return true;
+ } catch (err) {
+ if (err instanceof Error) {
+ error(i`Failed to get latest version number. (${err.message})`);
+ log(err.stack || '');
+ }
+ }
+ return false;
+ }
+
+ // dump version information
+ log(i`${product} version information\n`);
+ log(i` version: ${Version} `);
+
+
+ // Make the NOTICE and LICENSE files discoverable. NOTICE is generated during the official build.
+ log(i`Usage of vcpkg-ce is subject to license terms available at ${session.homeFolder.join('LICENSE.txt').fsPath}`);
+ log(i`Third-party license information is available at ${session.homeFolder.join('NOTICE.txt').fsPath}`);
+ return true;
+ }
+
+}
diff --git a/ce/ce/cli/constants.ts b/ce/ce/cli/constants.ts
new file mode 100644
index 0000000000..6e15e31800
--- /dev/null
+++ b/ce/ce/cli/constants.ts
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+export const cli = 'ce';
+export const product = 'vcpkg-ce';
+export const project = 'vcpkg-configuration.json';
+export const blank = '\n';
diff --git a/ce/ce/cli/format.ts b/ce/ce/cli/format.ts
new file mode 100644
index 0000000000..720914b5a6
--- /dev/null
+++ b/ce/ce/cli/format.ts
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { bold, cyan, gray, green, greenBright, grey, underline, whiteBright, yellowBright } from 'chalk';
+import { Uri } from '../util/uri';
+
+export function projectFile(uri: Uri): string {
+ return cyan(uri.fsPath);
+}
+
+export function artifactIdentity(registryName: string, identity: string, alias?: string) {
+ if (alias) {
+ return `${registryName}:${identity.substr(0, identity.length - alias.length)}${yellowBright(alias)}`;
+ }
+ return yellowBright(identity);
+}
+
+export function artifactReference(registryName: string, identity: string, version: string) {
+ return `${artifactIdentity(registryName, identity)}-v${gray(version)}`;
+}
+
+export function heading(text: string, level = 1) {
+ switch (level) {
+ case 1:
+ return `${underline.bold(text)}\n`;
+ case 2:
+ return `${greenBright(text)}\n`;
+ case 3:
+ return `${green(text)}\n`;
+ }
+ return `${bold(text)}\n`;
+}
+
+export function optional(text: string) {
+ return gray(text);
+}
+export function cmdSwitch(text: string) {
+ return optional(`--${text}`);
+}
+
+export function command(text: string) {
+ return whiteBright.bold(text);
+}
+
+export function hint(text: string) {
+ return green.dim(text);
+}
+
+export function count(num: number) {
+ return grey(`${num}`);
+}
+
+export function position(text: string) {
+ return grey(`${text}`);
+}
\ No newline at end of file
diff --git a/ce/ce/cli/markdown-table.ts b/ce/ce/cli/markdown-table.ts
new file mode 100644
index 0000000000..5359dec6bd
--- /dev/null
+++ b/ce/ce/cli/markdown-table.ts
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { strict } from 'assert';
+
+export class Table {
+ private readonly rows = new Array();
+ private numberOfColumns = 0;
+ constructor(...columnNames: Array) {
+ this.numberOfColumns = columnNames.length;
+ this.rows.push(`|${columnNames.join('|')}|`);
+ this.rows.push(`${'|--'.repeat(this.numberOfColumns)}|`);
+ }
+ push(...values: Array) {
+ strict.equal(values.length, this.numberOfColumns, 'unexpected number of arguments in table row');
+ this.rows.push(`|${values.join('|')}|`);
+ }
+ toString() {
+ return this.rows.join('\n');
+ }
+}
\ No newline at end of file
diff --git a/ce/ce/cli/project.ts b/ce/ce/cli/project.ts
new file mode 100644
index 0000000000..0ad6b5bdfd
--- /dev/null
+++ b/ce/ce/cli/project.ts
@@ -0,0 +1,65 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { ArtifactMap, ProjectManifest } from '../artifacts/artifact';
+import { i } from '../i18n';
+import { trackActivation } from '../insights';
+import { session } from '../main';
+import { Uri } from '../util/uri';
+import { installArtifacts, showArtifacts } from './artifacts';
+import { blank } from './constants';
+import { projectFile } from './format';
+import { error, log } from './styling';
+
+class ActivationOptions {
+ force?: boolean;
+ allLanguages?: boolean;
+ language?: string;
+ msbuildProps?: Uri;
+}
+
+export async function openProject(location: Uri): Promise {
+ // load the project
+ return new ProjectManifest(session, await session.openManifest(location));
+}
+
+export async function activate(artifacts: ArtifactMap, options?: ActivationOptions) {
+ // install the items in the project
+ const [success, artifactStatus, activation] = await installArtifacts(session, artifacts.artifacts, options);
+
+ if (success) {
+ // create an MSBuild props file if indicated by the user
+ const propsFile = options?.msbuildProps;
+ if (propsFile) {
+ await propsFile.writeUTF8(activation.generateMSBuild(artifactStatus.keys()));
+ }
+
+ // activate all the tools in the project
+ await session.setActivationInPostscript(activation);
+ }
+
+ return success;
+}
+
+export async function activateProject(project: ProjectManifest, options?: ActivationOptions) {
+ // track what got installed
+ const artifacts = await project.resolveDependencies();
+
+ // print the status of what is going to be activated.
+ if (!await showArtifacts(artifacts.artifacts, options)) {
+ error(i`Unable to activate project`);
+ return false;
+ }
+
+ if (await activate(artifacts, options)) {
+ trackActivation();
+ log(blank);
+ log(i`Project ${projectFile(project.metadata.context.folder)} activated`);
+ return true;
+ }
+
+ log(blank);
+ log(i`Failed to activate project ${projectFile(project.metadata.context.folder)}`);
+
+ return false;
+}
diff --git a/ce/ce/cli/styling.ts b/ce/ce/cli/styling.ts
new file mode 100644
index 0000000000..8b9676b44f
--- /dev/null
+++ b/ce/ce/cli/styling.ts
@@ -0,0 +1,104 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { blue, cyan, gray, green, red, white, yellow } from 'chalk';
+import * as renderer from 'marked-terminal';
+import { argv } from 'process';
+import { Session } from '../session';
+import { CommandLine } from './command-line';
+
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const marked = require('marked');
+
+function formatTime(t: number) {
+ return (
+ t < 3600000 ? [Math.floor(t / 60000) % 60, Math.floor(t / 1000) % 60, t % 1000] :
+ t < 86400000 ? [Math.floor(t / 3600000) % 24, Math.floor(t / 60000) % 60, Math.floor(t / 1000) % 60, t % 1000] :
+ [Math.floor(t / 86400000), Math.floor(t / 3600000) % 24, Math.floor(t / 60000) % 60, Math.floor(t / 1000) % 60, t % 1000]).map(each => each.toString().padStart(2, '0')).join(':').replace(/(.*):(\d)/, '$1.$2');
+}
+
+// setup markdown renderer
+marked.setOptions({
+ renderer: new renderer({
+ tab: 2,
+ emoji: true,
+ showSectionPrefix: false,
+ firstHeading: green.underline.bold,
+ heading: green.underline,
+ codespan: white.bold,
+ link: blue.bold,
+ href: blue.bold.underline,
+ code: gray,
+ tableOptions: {
+ chars: {
+ 'top': '', 'top-mid': '', 'top-left': '', 'top-right': ''
+ , 'bottom': '', 'bottom-mid': '', 'bottom-left': '', 'bottom-right': ''
+ , 'left': '', 'left-mid': '', 'mid': '', 'mid-mid': ''
+ , 'right': '', 'right-mid': '', 'middle': ''
+ }
+ }
+ }),
+ gfm: true,
+});
+
+export function indent(text: string): string
+export function indent(text: Array): Array
+export function indent(text: string | Array): string | Array {
+ if (Array.isArray(text)) {
+ return text.map(each => indent(each));
+ }
+ return ` ${text}`;
+}
+
+function md(text = '', session?: Session): string {
+ if (text) {
+ text = marked.marked(`${text}`.replace(/\\\./g, '\\\\.')); // work around md messing up paths with .\ in them.
+
+ // rewrite file:// urls to be locl filesystem urls.
+ return (!!text && !!session) ? text.replace(/(file:\/\/\S*)/g, (s, a) => yellow.dim(session.parseUri(a).fsPath)) : text;
+ }
+ return '';
+}
+
+const stdout = console['log'];
+
+export let log: (message?: any, ...optionalParams: Array) => void = stdout;
+export let error: (message?: any, ...optionalParams: Array) => void = console.error;
+export let warning: (message?: any, ...optionalParams: Array) => void = console.error;
+export let debug: (message?: any, ...optionalParams: Array) => void = (text) => {
+ if (argv.any(arg => arg === '--debug')) {
+ stdout(`${cyan.bold('debug: ')}${text}`);
+ }
+};
+
+export function writeException(e: any) {
+ if (e instanceof Error) {
+ debug(e.message);
+ debug(e.stack);
+ return;
+ }
+ debug(e && e.toString ? e.toString() : e);
+}
+
+export function initStyling(commandline: CommandLine, session: Session) {
+ log = (text) => stdout((md(text, session).trim()));
+ error = (text) => stdout(`${red.bold('ERROR: ')}${md(text, session).trim()}`);
+ warning = (text) => stdout(`${yellow.bold('WARNING: ')}${md(text, session).trim()}`);
+ debug = (text) => { if (commandline.debug) { stdout(`${cyan.bold('DEBUG: ')}${md(text, session).trim()}`); } };
+
+ session.channels.on('message', (text: string, context: any, msec: number) => {
+ log(text);
+ });
+
+ session.channels.on('error', (text: string, context: any, msec: number) => {
+ error(text);
+ });
+
+ session.channels.on('debug', (text: string, context: any, msec: number) => {
+ debug(`${cyan.bold(`[${formatTime(msec)}]`)} ${md(text, session)}`);
+ });
+
+ session.channels.on('warning', (text: string, context: any, msec: number) => {
+ warning(text);
+ });
+}
diff --git a/ce/ce/cli/switch.ts b/ce/ce/cli/switch.ts
new file mode 100644
index 0000000000..d0370b9e95
--- /dev/null
+++ b/ce/ce/cli/switch.ts
@@ -0,0 +1,49 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { strict } from 'assert';
+import { i } from '../i18n';
+import { Command } from './command';
+import { Help } from './command-line';
+import { cmdSwitch } from './format';
+
+
+export abstract class Switch implements Help {
+ readonly abstract switch: string;
+ readonly title = '';
+ readonly abstract help: Array;
+ readonly required: boolean;
+ readonly multipleAllowed: boolean;
+
+ constructor(protected command: Command, options?: { multipleAllowed?: boolean, required?: boolean }) {
+ command.switches.push(this);
+ this.multipleAllowed = options?.multipleAllowed || false;
+ this.required = options?.required || false;
+ }
+
+ get valid() {
+ return this.required || this.active;
+ }
+
+ #values?: Array;
+ get values() {
+ return this.#values || (this.#values = this.command.commandLine.claim(this.switch) || []);
+ }
+
+ get value(): any | undefined {
+ const v = this.values;
+ strict.ok(v.length < 2, i`Expected a single value for ${cmdSwitch(this.switch)} - found multiple`);
+ return v[0];
+ }
+
+ get requiredValue(): string {
+ const v = this.values;
+ strict.ok(v.length == 1 && v[0], i`Expected a single value for '--${this.switch}'.`);
+ return v[0];
+ }
+
+ get active(): boolean {
+ const v = this.values;
+ return !!v && v.length > 0 && v[0] !== 'false';
+ }
+}
diff --git a/ce/ce/cli/switches/clear.ts b/ce/ce/cli/switches/clear.ts
new file mode 100644
index 0000000000..3bb39ad0c7
--- /dev/null
+++ b/ce/ce/cli/switches/clear.ts
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { i } from '../../i18n';
+import { Switch } from '../switch';
+
+export class Clear extends Switch {
+ switch = 'clear';
+ get help() {
+ return [
+ i`removes all files in the local cache`
+ ];
+ }
+}
diff --git a/ce/ce/cli/switches/debug.ts b/ce/ce/cli/switches/debug.ts
new file mode 100644
index 0000000000..81b685af56
--- /dev/null
+++ b/ce/ce/cli/switches/debug.ts
@@ -0,0 +1,15 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { i } from '../../i18n';
+import { cli } from '../constants';
+import { Switch } from '../switch';
+
+export class Debug extends Switch {
+ switch = 'debug';
+ get help() {
+ return [
+ i`enables debug mode, displays internal messsages about how ${cli} works`
+ ];
+ }
+}
diff --git a/ce/ce/cli/switches/force.ts b/ce/ce/cli/switches/force.ts
new file mode 100644
index 0000000000..4c22d2cccb
--- /dev/null
+++ b/ce/ce/cli/switches/force.ts
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { i } from '../../i18n';
+import { Switch } from '../switch';
+
+export class Force extends Switch {
+ switch = 'force';
+ get help() {
+ return [
+ i`proceeds with the (potentially dangerous) action without confirmation`
+ ];
+ }
+}
diff --git a/ce/ce/cli/switches/installed.ts b/ce/ce/cli/switches/installed.ts
new file mode 100644
index 0000000000..aa56fe8763
--- /dev/null
+++ b/ce/ce/cli/switches/installed.ts
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { i } from '../../i18n';
+import { Switch } from '../switch';
+
+export class Installed extends Switch {
+ switch = 'installed';
+ get help() {
+ return [
+ i`shows the _installed_ artifacts`
+ ];
+ }
+}
diff --git a/ce/ce/cli/switches/msbuild-props.ts b/ce/ce/cli/switches/msbuild-props.ts
new file mode 100644
index 0000000000..125edc8c61
--- /dev/null
+++ b/ce/ce/cli/switches/msbuild-props.ts
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { i } from '../../i18n';
+import { session } from '../../main';
+import { Uri } from '../../util/uri';
+import { resolvePath } from '../command-line';
+import { Switch } from '../switch';
+
+export class MSBuildProps extends Switch {
+ switch = 'msbuild-props';
+ override multipleAllowed = false;
+ get help() {
+ return [
+ i`Full path to the file in which MSBuild properties will be written.`
+ ];
+ }
+
+ override get value(): Uri | undefined {
+ const v = resolvePath(super.value);
+ return v ? session.fileSystem.file(v) : undefined;
+ }
+}
diff --git a/ce/ce/cli/switches/project.ts b/ce/ce/cli/switches/project.ts
new file mode 100644
index 0000000000..3afca8b20c
--- /dev/null
+++ b/ce/ce/cli/switches/project.ts
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { ProjectManifest } from '../../artifacts/artifact';
+import { FileType } from '../../fs/filesystem';
+import { i } from '../../i18n';
+import { session } from '../../main';
+import { Uri } from '../../util/uri';
+import { resolvePath } from '../command-line';
+import { projectFile } from '../format';
+import { debug, error } from '../styling';
+import { Switch } from '../switch';
+
+export class Project extends Switch {
+ switch = 'project';
+ get help() {
+ return [
+ i`override the path to the project folder`
+ ];
+ }
+
+ async getProjectFolder() {
+ const v = resolvePath(super.value);
+ if (v) {
+ const uri = session.fileSystem.file(v);
+ const stat = await uri.stat();
+
+ if (stat.type & FileType.File) {
+ return uri;
+ }
+ if (stat.type & FileType.Directory) {
+ const project = await session.findProjectProfile(uri, false);
+ if (project) {
+ return project;
+ }
+ }
+ error(i`Unable to find project environment ${projectFile(uri)}`);
+ return undefined;
+ }
+ return session.findProjectProfile();
+ }
+
+ override get value(): Promise {
+ return this.getProjectFolder();
+ }
+
+ get manifest(): Promise {
+ return this.value.then(async (project) => {
+ if (!project) {
+ debug('No project manifest');
+ return undefined;
+ }
+
+ debug(`Loading project manifest ${project} `);
+ return new ProjectManifest(session, await session.openManifest(project));
+ });
+ }
+}
\ No newline at end of file
diff --git a/ce/ce/cli/switches/registry.ts b/ce/ce/cli/switches/registry.ts
new file mode 100644
index 0000000000..4f26c667a9
--- /dev/null
+++ b/ce/ce/cli/switches/registry.ts
@@ -0,0 +1,48 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { sanitizeUri } from '../../artifacts/artifact';
+import { i } from '../../i18n';
+import { Session } from '../../session';
+import { UpdateCommand } from '../commands/update';
+import { Switch } from '../switch';
+
+export class Registry extends Switch {
+ switch = 'registry';
+ get help() {
+ return [
+ i`override the path to the registry`
+ ];
+ }
+
+ async loadRegistries(session: Session, more: Array = []) {
+ for (const registry of new Set([...this.values, ...more].map(each => sanitizeUri(each)))) {
+ if (registry) {
+ const uri = session.parseUri(registry);
+ if (await session.isLocalRegistry(uri) || await session.isRemoteRegistry(uri)) {
+
+ const r = session.loadRegistry(uri, 'artifact');
+ if (r) {
+ try {
+ await r.load();
+ } catch (e) {
+ // try to update the repo
+ if (!await UpdateCommand.update(r)) {
+ session.channels.error(i`failed to load registry ${uri.toString()}`);
+ continue;
+ }
+ }
+ // registry is loaded
+ // it should be added to the aggregator
+ session.defaultRegistry.add(r, registry);
+ }
+ continue;
+ }
+ session.channels.error(i`Invalid registry ${registry}`);
+ }
+ }
+
+ return session.defaultRegistry;
+ }
+
+}
\ No newline at end of file
diff --git a/ce/ce/cli/switches/verbose.ts b/ce/ce/cli/switches/verbose.ts
new file mode 100644
index 0000000000..7eed7f7e05
--- /dev/null
+++ b/ce/ce/cli/switches/verbose.ts
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { i } from '../../i18n';
+import { Switch } from '../switch';
+
+export class Verbose extends Switch {
+ switch = 'verbose';
+ get help() {
+ return [
+ i`enables verbose mode, displays verbose messsages about the process`
+ ];
+ }
+}
diff --git a/ce/ce/cli/switches/version.ts b/ce/ce/cli/switches/version.ts
new file mode 100644
index 0000000000..e697b91a7c
--- /dev/null
+++ b/ce/ce/cli/switches/version.ts
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { i } from '../../i18n';
+import { Switch } from '../switch';
+
+export class Version extends Switch {
+ switch = 'version';
+ get help() {
+ return [
+ i`a version or version range to match`
+ ];
+ }
+}
\ No newline at end of file
diff --git a/ce/ce/cli/switches/whatIf.ts b/ce/ce/cli/switches/whatIf.ts
new file mode 100644
index 0000000000..4ffb9691bd
--- /dev/null
+++ b/ce/ce/cli/switches/whatIf.ts
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { i } from '../../i18n';
+import { Switch } from '../switch';
+
+export class WhatIf extends Switch {
+ switch = 'what-if';
+ get help() {
+ return [
+ i`does not actually perform the action, shows only what would be done`
+ ];
+ }
+}
diff --git a/ce/ce/constants.ts b/ce/ce/constants.ts
new file mode 100644
index 0000000000..b9b6511858
--- /dev/null
+++ b/ce/ce/constants.ts
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+export const project = 'environment.yaml';
+export const undo = 'Z_VCPKG_UNDO';
+export const postscriptVarible = 'Z_VCPKG_POSTSCRIPT';
+export const blank = '\n';
+export const gitUniqueIdPrefix = 'https://aka.ms/vcpkg-ce-default::tools/git::';
+export const gitArtifact = 'microsoft:tools/git';
+export const latestVersion = '*';
+export const vcpkgDownloadFolder = 'VCPKG_DOWNLOADS';
+export const globalConfigurationFile = 'vcpkg-configuration.global.json';
+export const profileNames = ['vcpkg-configuration.json', 'vcpkg-configuration.yaml', 'environment.yaml', 'environment.yml', 'environment.json'];
+export const registryIndexFile = 'index.yaml';
+
+export const defaultConfig =
+ `{
+ "registries": [
+ {
+ "kind": "artifact",
+ "name": "microsoft",
+ "location": "https://aka.ms/vcpkg-ce-default"
+ }
+ ]
+}
+`;
diff --git a/ce/ce/exports.ts b/ce/ce/exports.ts
new file mode 100644
index 0000000000..7fc8613767
--- /dev/null
+++ b/ce/ce/exports.ts
@@ -0,0 +1,167 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { ManyMap } from './util/linq';
+import { Queue } from './util/promise';
+
+/** This adds the expected declarations to the Array type. */
+declare global {
+ interface Array {
+ /**
+ * Returns the elements of an array that meet the condition specified in a callback function.
+ * @param callbackfn A function that accepts up to three arguments. The filter method calls the callbackfn function one time for each element in the array.
+ */
+ where(callbackfn: (value: T, index: number, array: Array) => value is S): Array;
+ /**
+ * Returns the elements of an array that meet the condition specified in a callback function.
+ * @param callbackfn A function that accepts up to three arguments. The filter method calls the callbackfn function one time for each element in the array.
+ */
+ where(callbackfn: (value: T, index: number, array: Array) => unknown): Array;
+
+ /**
+ * Calls a defined callback function on each element of an array, and returns an array that contains the results.
+ */
+ select(callbackfn: (value: T, index: number, array: Array) => U): Array;
+
+ /**
+ * Determines whether the specified callback function returns true for any element of an array.
+ * @param callbackfn A function that accepts up to three arguments. The some method calls
+ * the callbackfn function for each element in the array until the callbackfn returns a value
+ * which is coercible to the Boolean value true, or until the end of the array.
+ * @param thisArg An object to which the this keyword can refer in the callbackfn function.
+ * If thisArg is omitted, undefined is used as the this value.
+ */
+ any(callbackfn: (value: T, index: number, array: Array) => unknown, thisArg?: any): boolean;
+ /**
+ * Determines whether all the members of an array satisfy the specified test.
+ * @param callbackfn A function that accepts up to three arguments. The every method calls
+ * the callbackfn function for each element in the array until the callbackfn returns a value
+ * which is coercible to the Boolean value false, or until the end of the array.
+ * @param thisArg An object to which the this keyword can refer in the callbackfn function.
+ * If thisArg is omitted, undefined is used as the this value.
+ */
+ all(callbackfn: (value: T, index: number, array: Array) => unknown, thisArg?: any): boolean;
+
+ /**
+ * Removes elements from an array and, if necessary, inserts new elements in their place, returning the deleted elements.
+ * @param start The zero-based location in the array from which to start removing elements.
+ * @param deleteCount The number of elements to remove.
+ * @param items Elements to insert into the array in place of the deleted elements.
+ */
+ insert(start: number, ...items: Array): Array;
+
+ /**
+ * Removes elements from an array returning the deleted elements.
+ * @param start The zero-based location in the array from which to start removing elements.
+ * @param deleteCount The number of elements to remove.
+ */
+ remove(start: number, deleteCount?: number): Array;
+
+ /**
+ * Iterates on a collection to create a Queue that will throttle
+ * the async operation 'fn' to a reasonable degree of parallelism.
+ * @param fn the async Fn to call on each
+ */
+ forEachAsync(fn: (v: T) => Promise): Queue;
+
+ selectMany(callbackfn: (value: T, index: number, array: Array) => U): Array ? InnerArr : U>;
+ groupByMap(keySelector: (each: T) => TKey, selector: (each: T) => TValue): Map>;
+ groupBy(keySelector: (each: T) => string, selector: (each: T) => TValue): { [s: string]: Array };
+ count(predicate: (each: T) => Promise): Promise,
+ count(predicate: (each: T) => boolean): number,
+ readonly last: T | undefined;
+ readonly first: T | undefined;
+ }
+}
+
+declare global {
+ interface Map {
+ getOrDefault(key: K, defaultValue: V | (() => V)): V;
+ }
+}
+
+if (!Map.prototype.getOrDefault) {
+ Object.defineProperties(Map.prototype, {
+ getOrDefault: {
+ value: function (key: any, defaultValue: any) {
+ let v = this.get(key);
+ if (!v) {
+ this.set(key, v = typeof defaultValue === 'function' ? defaultValue() : defaultValue);
+ }
+ return v;
+ }
+ }
+ });
+}
+
+if (!Array.prototype.insert) {
+ /**
+ * adding some linq-like functionality to the Array type
+ */
+ Object.defineProperties(Array.prototype, {
+ where: { value: Array.prototype.filter },
+ select: { value: Array.prototype.map },
+ any: { value: Array.prototype.some },
+ all: { value: Array.prototype.every },
+ insert: { value: function (position: number, items: Array) { return (>this).splice(position, 0, ...items); } },
+ selectMany: { value: Array.prototype.flatMap },
+ count: {
+ value: function (predicate: (e: any) => boolean | Promise) {
+ let v = 0;
+ const all = [];
+ for (const each of this) {
+ const test = predicate(each);
+ if (test.then) {
+ all.push(test.then((antecedent: any) => {
+ if (antecedent) {
+ v++;
+ }
+ }));
+ continue;
+ }
+ if (test) {
+ v++;
+ }
+ }
+ if (all.length) {
+ return Promise.all(all).then(() => v);
+ }
+ return v;
+ }
+ },
+ groupByMap: {
+ value: function (keySelector: (each: any) => any, selector: (each: any) => any) {
+ const result = new ManyMap();
+ for (const each of this) {
+ result.push(keySelector(each), selector(each));
+ }
+ return result;
+ }
+ },
+ groupBy: {
+ value: function (keySelector: (each: any) => any, selector: (each: any) => any) {
+ const result = {};
+ for (const each of this) {
+ const key = keySelector(each);
+ (result[key] = result[key] || new Array()).push(selector(each));
+ }
+ return result;
+ }
+ },
+ last: {
+ get() {
+ return this[this.length - 1];
+ }
+ },
+ first: {
+ get() {
+ return this[0];
+ }
+ },
+ forEachAsync: {
+ value: function (fn: (i: any) => Promise) {
+ return new Queue().enqueueMany(this, fn);
+ }
+ }
+ });
+}
diff --git a/ce/ce/fs/acquire.ts b/ce/ce/fs/acquire.ts
new file mode 100644
index 0000000000..d930ac524c
--- /dev/null
+++ b/ce/ce/fs/acquire.ts
@@ -0,0 +1,249 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { strict } from 'assert';
+import { pipeline as origPipeline } from 'stream';
+import { promisify } from 'util';
+import { i } from '../i18n';
+import { AcquireEvents } from '../interfaces/events';
+import { Session } from '../session';
+import { Credentials } from '../util/credentials';
+import { ExtendedEmitter } from '../util/events';
+import { RemoteFileUnavailable } from '../util/exceptions';
+import { Algorithm, Hash } from '../util/hash';
+import { Uri } from '../util/uri';
+import { get, getStream, RemoteFile, resolveRedirect } from './https';
+import { ProgressTrackingStream } from './streams';
+
+const pipeline = promisify(origPipeline);
+
+const size32K = 1 << 15;
+const size64K = 1 << 16;
+
+export interface AcquireOptions extends Hash {
+ /** force a redownload even if it's in cache */
+ force?: boolean;
+ credentials?: Credentials;
+}
+
+export async function acquireArtifactFile(session: Session, uris: Array, outputFilename: string, events: Partial, options?: AcquireOptions) {
+ await session.cache.createDirectory();
+ const outputFile = session.cache.join(outputFilename);
+ session.channels.debug(`Acquire file '${outputFilename}' from [${uris.map(each => each.toString()).join(',')}]`);
+
+ if (options?.algorithm && options?.value) {
+ session.channels.debug(`We have a hash: ${options.algorithm}/${options.value}`);
+
+ // if we have hash data, check to see if the output file is good.
+ if (await outputFile.isFile()) {
+ session.channels.debug(`There is an output file already, verifying: ${outputFile.fsPath}`);
+
+ if (await outputFile.hashValid(events, options)) {
+ session.channels.debug(`Cached file matched hash: ${outputFile.fsPath}`);
+ return outputFile;
+ }
+ }
+ }
+
+ // is the file present on a local filesystem?
+ for (const uri of uris) {
+ if (uri.isLocal) {
+ // we have a local file
+
+ if (options?.algorithm && options?.value) {
+ // we have a hash.
+ // is it valid?
+ if (await uri.hashValid(events, options)) {
+ session.channels.debug(`Local file matched hash: ${uri.fsPath}`);
+ return uri;
+ }
+ } else if (await uri.exists()) {
+ // we don't have a hash, but the file is local, and it exists.
+ // we have to return it
+ session.channels.debug(`Using local file (no hash, unable to verify): ${uri.fsPath}`);
+ return uri;
+ }
+ // do we have a filename
+ }
+ }
+
+ // we don't have a local file
+ // https is all that we know at the moment.
+ const webUris = uris.where(each => each.isHttps);
+ if (webUris.length === 0) {
+ // wait, no web uris?
+ throw new RemoteFileUnavailable(uris);
+ }
+
+ return https(session, webUris, outputFilename, events, options);
+}
+
+/** */
+async function https(session: Session, uris: Array, outputFilename: string, events: Partial, options?: AcquireOptions) {
+ const ee = new ExtendedEmitter();
+ ee.subscribe(events);
+ session.channels.debug(`Attempting to download file '${outputFilename}' from [${uris.map(each => each.toString()).join(',')}]`);
+
+ let resumeAtOffset = 0;
+ await session.cache.createDirectory();
+ const outputFile = session.cache.join(outputFilename);
+
+ if (options?.force) {
+ session.channels.debug(`Acquire '${outputFilename}': force specified, forcing download`);
+ // is force specified; delete the current file
+ await outputFile.delete();
+ }
+
+ // start this peeking at the target uris.
+ session.channels.debug(`Acquire '${outputFilename}': checking remote connections`);
+ const locations = new RemoteFile(uris, { credentials: options?.credentials });
+ let url: Uri | undefined;
+
+ // is there a file in the cache
+ if (await outputFile.exists()) {
+ session.channels.debug(`Acquire '${outputFilename}': local file exists`);
+ if (options?.algorithm) {
+ // does it match a hash that we have?
+ if (await outputFile.hashValid(events, options)) {
+ session.channels.debug(`Acquire '${outputFilename}': local file hash matches metdata`);
+ // yes it does. let's just return done.
+ return outputFile;
+ }
+ }
+
+ // it doesn't match a known hash.
+ const contentLength = await locations.contentLength;
+ session.channels.debug(`Acquire '${outputFilename}': remote connection info is back`);
+ const onDiskSize = await outputFile.size();
+ if (!await locations.availableLocation) {
+ if (locations.failures.all(each => each.code === 404)) {
+ let msg = i`Unable to download file`;
+ if (options?.credentials) {
+ msg += (i` - It could be that your authentication credentials are not correct`);
+ }
+
+ session.channels.error(msg);
+ throw new RemoteFileUnavailable(uris);
+ }
+ }
+ // first, make sure that there is a remote that is accessible.
+ strict.ok(!!await locations.availableLocation, `Requested file ${outputFilename} has no accessible locations ${uris.map(each => each.toString()).join(',')}`);
+
+ url = await locations.resumableLocation;
+ // ok, does it support resume?
+ if (url) {
+ // yes, let's check what the size is expected to be.
+
+ if (!options?.algorithm) {
+
+ if (contentLength === onDiskSize) {
+ session.channels.debug(`Acquire '${outputFilename}': on disk file matches length of remote file`);
+ const algorithm = (await locations.algorithm);
+ const value = await locations.hash;
+ session.channels.debug(`Acquire '${outputFilename}': remote alg/hash: '${algorithm}'/'${value}`);
+ if (algorithm && value && outputFile.hashValid(events, { algorithm, value, ...options })) {
+ session.channels.debug(`Acquire '${outputFilename}': on disk file hash matches the server hash`);
+ // so *we* don't have the hash, but ... if the server has a hash, we could see if what we have is what they have?
+ // it does match what the server has.
+ // I call this an win.
+ return outputFile;
+ }
+
+ // we don't have a hash, or what we have doesn't match.
+ // maybe we will get a match below (or resume)
+ }
+ }
+
+ if (onDiskSize > size64K) {
+ // it's bigger than 64k. Good. otherwise, we're just wasting time.
+
+ // so, how big is the remote
+ if (contentLength >= onDiskSize) {
+ session.channels.debug(`Acquire '${outputFilename}': local file length is less than or equal to remote file length`);
+ // looks like there could be more remotely than we have.
+ // lets compare the first 32k and the last 32k of what we have
+ // against what they have and see if they match.
+ const top = (await get(url, { start: 0, end: size32K - 1, credentials: options?.credentials })).rawBody;
+ const bottom = (await get(url, { start: onDiskSize - size32K, end: onDiskSize - 1, credentials: options?.credentials })).rawBody;
+
+ const onDiskTop = await outputFile.readBlock(0, size32K - 1);
+ const onDiskBottom = await outputFile.readBlock(onDiskSize - size32K, onDiskSize - 1);
+
+ if (top.compare(onDiskTop) === 0 && bottom.compare(onDiskBottom) === 0) {
+ session.channels.debug(`Acquire '${outputFilename}': first/last blocks are equal`);
+ // the start and end of what we have does match what they have.
+ // is this file the same size?
+ if (contentLength === onDiskSize) {
+ // same file size, front and back match, let's accept this. begrudgingly
+ session.channels.debug(`Acquire '${outputFilename}': file size is identical. keeping this one`);
+ return outputFile;
+ }
+ // looks like we can continue from here.
+ session.channels.debug(`Acquire '${outputFilename}': ok to resume`);
+ resumeAtOffset = onDiskSize;
+ }
+ }
+ }
+ }
+ }
+
+ if (resumeAtOffset === 0) {
+ // clearly we mean to not resume. clean any existing file.
+ session.channels.debug(`Acquire '${outputFilename}': not resuming file, full download`);
+ await outputFile.delete();
+ }
+
+ url = url || await locations.availableLocation;
+ strict.ok(!!url, `Requested file ${outputFilename} has no accessible locations ${uris.map(each => each.toString()).join(',')}`);
+ session.channels.debug(`Acquire '${outputFilename}': initiating download`);
+ const length = await locations.contentLength;
+
+ const inputStream = getStream(url, { start: resumeAtOffset, end: length > 0 ? length : undefined, credentials: options?.credentials });
+ let progressStream;
+ if (length > 0) {
+ progressStream = new ProgressTrackingStream(resumeAtOffset, length);
+ progressStream.on('progress', (filePercentage) => ee.emit('download', outputFilename, filePercentage));
+ }
+ const outputStream = await outputFile.writeStream({ append: true });
+ ee.emit('download', outputFilename, 0);
+
+ // whoooosh. write out the file
+ if (progressStream) {
+ await pipeline(inputStream, progressStream, outputStream);
+ } else {
+ await pipeline(inputStream, outputStream);
+ }
+
+ // we've downloaded the file, let's see if it matches the hash we have.
+ if (options?.algorithm) {
+ session.channels.debug(`Acquire '${outputFilename}': checking downloaded file hash`);
+ // does it match the hash that we have?
+ if (!await outputFile.hashValid(events, options)) {
+ await outputFile.delete();
+ throw new Error(i`Downloaded file '${outputFile.fsPath}' did not have the correct hash (${options.algorithm}: ${options.value}) `);
+ }
+ session.channels.debug(`Acquire '${outputFilename}': downloaded file hash matches specified hash`);
+ }
+
+ session.channels.debug(`Acquire '${outputFilename}': downloading file successful`);
+ ee.emit('download', outputFilename, 1000);
+ ee.emit('complete');
+ return outputFile;
+}
+
+export async function resolveNugetUrl(session: Session, pkg: string) {
+ const [, name, version] = pkg.match(/^(.*)\/(.*)$/) ?? [];
+ strict.ok(version, i`package reference '${pkg}' is not a valid nuget package reference ({name}/{version})`);
+
+ // let's resolve the redirect first, since nuget servers don't like us getting HEAD data on the targets via a redirect.
+ // even if this wasn't the case, this is lower cost now rather than later.
+ const url = await resolveRedirect(session.parseUri(`https://www.nuget.org/api/v2/package/${name}/${version}`));
+
+ session.channels.debug(`Resolving nuget package for '${pkg}' to '${url}'`);
+ return url;
+}
+
+export async function acquireNugetFile(session: Session, pkg: string, outputFilename: string, events: Partial, options?: AcquireOptions): Promise {
+ return https(session, [await resolveNugetUrl(session, pkg)], outputFilename, events, options);
+}
+
diff --git a/ce/ce/fs/filesystem.ts b/ce/ce/fs/filesystem.ts
new file mode 100644
index 0000000000..fc54c4761c
--- /dev/null
+++ b/ce/ce/fs/filesystem.ts
@@ -0,0 +1,410 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+/* eslint-disable @typescript-eslint/ban-types */
+
+import { EventEmitter } from 'ee-ts';
+import { Readable, Writable } from 'stream';
+import { Session } from '../session';
+import { Uri } from '../util/uri';
+
+const size64K = 1 << 16;
+const size32K = 1 << 15;
+
+/**
+ * The `FileStat`-type represents metadata about a file
+ */
+export interface FileStat {
+ /**
+ * The type of the file, e.g. is a regular file, a directory, or symbolic link
+ * to a file.
+ *
+ * *Note:* This value might be a bitmask, e.g. `FileType.File | FileType.SymbolicLink`.
+ */
+ type: FileType;
+ /**
+ * The creation timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC.
+ */
+ ctime: number;
+ /**
+ * The modification timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC.
+ *
+ * *Note:* If the file changed, it is important to provide an updated `mtime` that advanced
+ * from the previous value. Otherwise there may be optimizations in place that will not show
+ * the updated file contents in an editor for example.
+ */
+ mtime: number;
+ /**
+ * The size in bytes.
+ *
+ * *Note:* If the file changed, it is important to provide an updated `size`. Otherwise there
+ * may be optimizations in place that will not show the updated file contents in an editor for
+ * example.
+ */
+ size: number;
+ /**
+ * The file mode (unix permissions).
+ */
+ mode: number;
+}
+
+/**
+* Enumeration of file types. The types `File` and `Directory` can also be
+* a symbolic links, in that case use `FileType.File | FileType.SymbolicLink` and
+* `FileType.Directory | FileType.SymbolicLink`.
+*/
+export enum FileType {
+ /**
+ * The file type is unknown.
+ */
+ Unknown = 0,
+ /**
+ * A regular file.
+ */
+ File = 1,
+ /**
+ * A directory.
+ */
+ Directory = 2,
+ /**
+ * A symbolic link to a file.
+ */
+ SymbolicLink = 64
+}
+
+export interface WriteStreamOptions {
+ append?: boolean;
+ mode?: number;
+ mtime?: Date;
+}
+
+/**
+ * A random-access reading interface to access a file in a FileSystem.
+ *
+ * Ideally, we keep reads in a file to a forward order, so that this can be implemented on filesystems
+ * that do not support random access (ie, please do your best to order reads so that they go forward only as much as possible)
+ *
+ * Underneath on FSes that do not support random access, this would likely require multiple 'open' operation for the same
+ * target file.
+ */
+export abstract class ReadHandle {
+ /**
+ * Reads a block from a file
+ *
+ * @param buffer The buffer that the data will be written to.
+ * @param offset The offset in the buffer at which to start writing.
+ * @param length The number of bytes to read.
+ * @param position The offset from the beginning of the file from which data should be read. If `null`, data will be read from the current position.
+ */
+ abstract read(buffer: TBuffer, offset?: number | null, length?: number | null, position?: number | null): Promise<{ bytesRead: number, buffer: TBuffer }>;
+
+ async readComplete(buffr: TBuffer, offset = 0, length = buffr.byteLength, position: number | null = null, totalRead = 0): Promise<{ bytesRead: number, buffer: TBuffer }> {
+ const { bytesRead, buffer } = await this.read(buffr, offset, length, position);
+ if (length) {
+ if (bytesRead && bytesRead < length) {
+ return await this.readComplete(buffr, offset + bytesRead, length - bytesRead, position ? position + bytesRead : null, bytesRead + totalRead);
+ }
+ }
+ return { bytesRead: bytesRead + totalRead, buffer };
+ }
+ /**
+ * Returns a Readable for consuming an opened ReadHandle
+ * @param start the first byte to read of the target
+ * @param end the last byte to read of the target (inclusive!)
+ */
+ readStream(start = 0, end = Infinity): Readable {
+ return Readable.from(asyncIterableOverHandle(start, end, this), {});
+ }
+
+ abstract size(): Promise;
+
+ abstract close(): Promise;
+
+ range(start: number, length: number) {
+ return new RangeReadHandle(this, start, length);
+ }
+}
+
+class RangeReadHandle extends ReadHandle {
+
+ pos = 0;
+ readHandle?: ReadHandle;
+
+ constructor(readHandle: ReadHandle, private start: number, private length: number) {
+ super();
+ this.readHandle = readHandle;
+ }
+
+ async read(buffer: TBuffer, offset?: number | null, length?: number | null, position?: number | null): Promise<{ bytesRead: number; buffer: TBuffer; }> {
+ if (this.readHandle) {
+ position = position !== undefined && position !== null ? (position + this.start) : (this.pos + this.start);
+ length = length === null ? this.length : length;
+
+ const result = await this.readHandle.read(buffer, offset, length, position);
+ this.pos += result.bytesRead;
+ return result;
+ }
+
+ return {
+ bytesRead: 0, buffer
+ };
+
+ }
+
+ async size(): Promise {
+ return this.length;
+ }
+
+ async close(): Promise {
+ this.readHandle = undefined;
+ }
+
+}
+
+/**
+ * Picks a reasonable buffer size. Not more than 64k
+ *
+ * @param length
+ */
+function reasonableBuffer(length: number) {
+ return Buffer.alloc(length > size64K ? size32K : length);
+}
+
+/**
+ * Creates an AsyncIterable over a ReadHandle
+ * @param start the first byte in the target read from
+ * @param end the last byte in the target to read from
+ * @param handle the ReadHandle
+ */
+async function* asyncIterableOverHandle(start: number, end: number, handle: ReadHandle): AsyncIterable {
+ while (start < end) {
+ // buffer alloc must be inside the loop; zlib will hold the buffers until it can deal with a whole stream.
+ const buffer = reasonableBuffer(1 + end - start);
+ const count = Math.min(1 + end - start, buffer.byteLength);
+ const b = await handle.read(buffer, 0, count, start);
+ if (b.bytesRead === 0) {
+ return;
+ }
+ start += b.bytesRead;
+ // return only what was actually read. (just a view)
+ if (b.bytesRead === buffer.byteLength) {
+ yield buffer;
+ }
+ else {
+ yield buffer.slice(0, b.bytesRead);
+ }
+ }
+}
+
+export abstract class FileSystem extends EventEmitter {
+
+ protected baseUri?: Uri;
+
+ /**
+ * Creates a new URI from a file system path, e.g. `c:\my\files`,
+ * `/usr/home`, or `\\server\share\some\path`.
+ *
+ * associates this FileSystem with the Uri
+ *
+ * @param path A file system path (see `URI#fsPath`)
+ */
+ file(path: string): Uri {
+ return Uri.file(this, path);
+ }
+
+ /** construct an Uri from the various parts */
+ from(components: {
+ scheme: string;
+ authority?: string;
+ path?: string;
+ query?: string;
+ fragment?: string;
+ }): Uri {
+ return Uri.from(this, components);
+ }
+
+ /**
+ * Creates a new URI from a string, e.g. `https://www.msft.com/some/path`,
+ * `file:///usr/home`, or `scheme:with/path`.
+ *
+ * @param value A string which represents an URI (see `URI#toString`).
+ */
+ parse(value: string, _strict?: boolean): Uri {
+ return Uri.parse(this, value, _strict);
+ }
+
+ /**
+ * Retrieve metadata about a file.
+ *
+ * @param uri The uri of the file to retrieve metadata about.
+ * @return The file metadata about the file.
+ */
+ abstract stat(uri: Uri, options?: {}): Promise;
+
+ /**
+ * Retrieve all entries of a [directory](#FileType.Directory).
+ *
+ * @param uri The uri of the folder.
+ * @return An array of name/type-tuples or a Promise that resolves to such.
+ */
+ abstract readDirectory(uri: Uri, options?: { recursive?: boolean }): Promise>;
+
+ /**
+ * Create a new directory (Note, that new files are created via `write`-calls).
+ *
+ * *Note* that missing directories are created automatically, e.g this call has
+ * `mkdirp` semantics.
+ *
+ * @param uri The uri of the new folder.
+ */
+ abstract createDirectory(uri: Uri, options?: {}): Promise;
+
+ /**
+ * Read the entire contents of a file.
+ *
+ * @param uri The uri of the file.
+ * @return An array of bytes or a Promise that resolves to such.
+ */
+ abstract readFile(uri: Uri, options?: {}): Promise;
+
+ /**
+ * Creates a stream to read a file from the filesystem
+ *
+ * @param uri The uri of the file.
+ * @return a Readable stream
+ */
+ abstract readStream(uri: Uri, options?: { start?: number, end?: number }): Promise;
+
+ /**
+ * Write data to a file, replacing its entire contents.
+ *
+ * @param uri The uri of the file.
+ * @param content The new content of the file.
+ */
+ abstract writeFile(uri: Uri, content: Uint8Array): Promise;
+
+ /**
+ * Creates a stream to write a file to the filesystem
+ *
+ * @param uri The uri of the file.
+ * @return a Writeable stream
+ */
+ abstract writeStream(uri: Uri, options?: WriteStreamOptions): Promise;
+
+ /**
+ * Delete a file.
+ *
+ * @param uri The resource that is to be deleted.
+ * @param options Defines if trash can should be used and if deletion of folders is recursive
+ */
+ abstract delete(uri: Uri, options?: { recursive?: boolean, useTrash?: boolean }): Promise;
+
+ /**
+ * Rename a file or folder.
+ *
+ * @param oldUri The existing file.
+ * @param newUri The new location.
+ * @param options Defines if existing files should be overwritten.
+ */
+ abstract rename(source: Uri, target: Uri, options?: { overwrite?: boolean }): Promise;
+
+ abstract openFile(uri: Uri): Promise;
+
+ /**
+ * Copy files or folders.
+ *
+ * @param source The existing file.
+ * @param destination The destination location.
+ * @param options Defines if existing files should be overwritten.
+ */
+ abstract copy(source: Uri, target: Uri, options?: { overwrite?: boolean }): Promise;
+
+ abstract createSymlink(symlink: Uri, target: Uri): Promise;
+
+ /** checks to see if the target exists */
+ async exists(uri: Uri) {
+ try {
+ return !!(await this.stat(uri));
+ } catch (e) {
+ // if this fails, we're assuming false
+ }
+ return false;
+ }
+
+ /** checks to see if the target is a directory/folder */
+ async isDirectory(uri: Uri) {
+ try {
+ return !!((await this.stat(uri)).type & FileType.Directory);
+ } catch {
+ // if this fails, we're assuming false
+ }
+ return false;
+ }
+
+ /** checks to see if the target is a file */
+ async isFile(uri: Uri) {
+ try {
+ const s = await this.stat(uri);
+
+ return !!(s.type & FileType.File);
+ } catch {
+ // if this fails, we're assuming false
+ }
+ return false;
+ }
+
+ /** checks to see if the target is a symbolic link */
+ async isSymlink(uri: Uri) {
+ try {
+ return !!((await this.stat(uri)) && FileType.SymbolicLink);
+ } catch {
+ // if this fails, we're assuming false
+ }
+ return false;
+ }
+
+ constructor(protected session: Session) {
+ super();
+ }
+
+ /** EventEmitter for when files are read */
+ protected read(path: Uri, context?: any) {
+ this.emit('read', path, context, this.session.stopwatch.total);
+ }
+
+ /** EventEmitter for when files are written */
+ protected write(path: Uri, context?: any) {
+ this.emit('write', path, context, this.session.stopwatch.total);
+ }
+
+ /** EventEmitter for when files are deleted */
+ protected deleted(path: Uri, context?: any) {
+ this.emit('deleted', path, context, this.session.stopwatch.total);
+ }
+
+ /** EventEmitter for when files are renamed */
+ protected renamed(path: Uri, context?: any) {
+ this.emit('renamed', path, context, this.session.stopwatch.total);
+ }
+
+ /** EventEmitter for when directories are read */
+ protected directoryRead(path: Uri, contents?: Promise>) {
+ this.emit('directoryRead', path, contents, this.session.stopwatch.total);
+ }
+
+ /** EventEmitter for when direcotries are created */
+ protected directoryCreated(path: Uri, context?: any) {
+ this.emit('directoryCreated', path, context, this.session.stopwatch.total);
+ }
+}
+
+
+/** Event definitions for FileSystem events */
+interface FileSystemEvents {
+ read(path: Uri, context: any, msec: number): void;
+ write(path: Uri, context: any, msec: number): void;
+ deleted(path: Uri, context: any, msec: number): void;
+ renamed(path: Uri, context: any, msec: number): void;
+ directoryRead(path: Uri, contents: Promise> | undefined, msec: number): void;
+ directoryCreated(path: Uri, context: any, msec: number): void;
+}
diff --git a/ce/ce/fs/http-filesystem.ts b/ce/ce/fs/http-filesystem.ts
new file mode 100644
index 0000000000..477934fdab
--- /dev/null
+++ b/ce/ce/fs/http-filesystem.ts
@@ -0,0 +1,93 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { Readable, Writable } from 'stream';
+import { Uri } from '../util/uri';
+import { FileStat, FileSystem, FileType, ReadHandle } from './filesystem';
+import { get, getStream, head } from './https';
+
+/**
+ * HTTPS Filesystem
+ *
+ */
+export class HttpsFileSystem extends FileSystem {
+
+ async stat(uri: Uri): Promise {
+ const result = await head(uri);
+
+ return {
+ type: FileType.File,
+ mtime: Date.parse(result.headers.date || ''),
+ ctime: Date.parse(result.headers.date || ''),
+ size: Number.parseInt(result.headers['content-length'] || '0'),
+ mode: 0o555 // https is read only but always 'executable'
+ };
+ }
+ readDirectory(uri: Uri): Promise> {
+ throw new Error('Method not implemented');
+ }
+ createDirectory(uri: Uri): Promise {
+ throw new Error('Method not implemented');
+ }
+ async readFile(uri: Uri): Promise {
+ return (await get(uri)).rawBody;
+ }
+ writeFile(uri: Uri, content: Uint8Array): Promise {
+ throw new Error('Method not implemented');
+ }
+ delete(uri: Uri, options?: { recursive?: boolean | undefined; useTrash?: boolean | undefined; }): Promise {
+ throw new Error('Method not implemented');
+ }
+ rename(source: Uri, target: Uri, options?: { overwrite?: boolean | undefined; }): Promise {
+ throw new Error('Method not implemented');
+ }
+ copy(source: Uri, target: Uri, options?: { overwrite?: boolean | undefined; }): Promise {
+ throw new Error('Method not implemented');
+ }
+ async createSymlink(original: Uri, symlink: Uri): Promise {
+ throw new Error('Method not implemented');
+ }
+ async readStream(uri: Uri, options?: { start?: number, end?: number }): Promise {
+ return getStream(uri, options);
+ }
+ writeStream(uri: Uri): Promise {
+ throw new Error('Method not implemented');
+ }
+
+ async openFile(uri: Uri): Promise {
+ return new HttpsReadHandle(uri);
+ }
+}
+
+
+class HttpsReadHandle extends ReadHandle {
+ position = 0;
+ constructor(private target: Uri) {
+ super();
+ }
+
+ async read(buffer: TBuffer, offset = 0, length = buffer.byteLength, position: number | null = null): Promise<{ bytesRead: number; buffer: TBuffer; }> {
+ if (position !== null) {
+ this.position = position;
+ }
+
+ const r = getStream(this.target, { start: this.position, end: this.position + length });
+ let bytesRead = 0;
+
+ for await (const chunk of r) {
+ const c = chunk;
+ c.copy(buffer, offset);
+ bytesRead += c.length;
+ offset += c.length;
+ }
+ return { bytesRead, buffer };
+ }
+
+ async size(): Promise {
+ return this.target.size();
+ }
+
+ async close() {
+ //return this.handle.close();
+ }
+}
diff --git a/ce/ce/fs/https.ts b/ce/ce/fs/https.ts
new file mode 100644
index 0000000000..9b95538b98
--- /dev/null
+++ b/ce/ce/fs/https.ts
@@ -0,0 +1,225 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { default as got, Headers, HTTPError, Response } from 'got';
+import { Credentials } from '../util/credentials';
+import { anyWhere } from '../util/promise';
+import { Uri } from '../util/uri';
+
+/**
+ * Resolves an HTTPS GET redirect by doing the GET, grabbing the redirects and then cancelling the rest of the request
+ * @param location the URL to get the final location of
+ */
+export async function resolveRedirect(location: Uri) {
+ let finalUrl = location;
+
+ const stream = got.get(location.toUrl(), { timeout: 15000, isStream: true });
+
+ // when the response comes thru, we can grab the headers & stuff from it
+ stream.on('response', (response: Response) => {
+ finalUrl = location.fileSystem.parse(response.redirectUrls.last || finalUrl.toString());
+ });
+
+ // we have to get at least some data for the response event to trigger.
+ for await (const chunk of stream) {
+ // but we don't need any of it :D
+ break;
+ }
+ stream.destroy();
+ return finalUrl;
+}
+
+/**
+ * Does an HTTPS HEAD request, and on a 404, tries to do an HTTPS GET and see if we get a redirect, and harvest the headers from that.
+ * @param location the target URL
+ * @param headers any headers to put in the request.
+ */
+export async function head(location: Uri, headers: Headers = {}, credentials?: Credentials): Promise> {
+ try {
+ setCredentials(headers, location, credentials);
+ // on a successful HEAD request, do nothing different
+ return await got.head(location.toUrl(), { timeout: 15000, headers });
+ } catch (E) {
+ // O_o
+ //
+ // So, it turns out that nuget servers (maybe others too?) don't do redirects on HEAD requests,
+ // and instead issue a 404.
+ // let's retry the request as a GET, and dump it after the first chunk.
+ // typically, a HEAD request should see a 300-400msec response time
+ // and yes, this does stretch that out to 500-700msec, but whatcha gonna do?
+ if (E instanceof HTTPError && E.response.statusCode === 404) {
+ try {
+ const syntheticResponse =