From 2f045577038f5a44fec53545d4d470281e318c1e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:34:16 +0000 Subject: [PATCH 1/5] Initial plan From c3da81872c7ef5ab251629760f0ee279fe44db3d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:50:35 +0000 Subject: [PATCH 2/5] Identified root cause - genContentTypesXml only processes vsix builder files Co-authored-by: dpaquette <2531875+dpaquette@users.noreply.github.com> --- .../extensionless-exec-test/README.md | 3 +++ .../extensionless-exec-test/linux-executable | 2 ++ .../vss-extension.json | 26 +++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 tests/extension-samples/extensionless-exec-test/README.md create mode 100755 tests/extension-samples/extensionless-exec-test/linux-executable create mode 100644 tests/extension-samples/extensionless-exec-test/vss-extension.json diff --git a/tests/extension-samples/extensionless-exec-test/README.md b/tests/extension-samples/extensionless-exec-test/README.md new file mode 100644 index 00000000..34b3caca --- /dev/null +++ b/tests/extension-samples/extensionless-exec-test/README.md @@ -0,0 +1,3 @@ +# Extensionless Executable Test + +This extension contains a Linux executable without a file extension. diff --git a/tests/extension-samples/extensionless-exec-test/linux-executable b/tests/extension-samples/extensionless-exec-test/linux-executable new file mode 100755 index 00000000..34157597 --- /dev/null +++ b/tests/extension-samples/extensionless-exec-test/linux-executable @@ -0,0 +1,2 @@ +#!/bin/bash +echo "This is a Linux executable without a file extension" diff --git a/tests/extension-samples/extensionless-exec-test/vss-extension.json b/tests/extension-samples/extensionless-exec-test/vss-extension.json new file mode 100644 index 00000000..9edac3c0 --- /dev/null +++ b/tests/extension-samples/extensionless-exec-test/vss-extension.json @@ -0,0 +1,26 @@ +{ + "manifestVersion": 1, + "id": "extensionless-exec-test", + "name": "Extensionless Executable Test", + "version": "1.0.0", + "publisher": "test-publisher", + "description": "Test extension with extensionless Linux executable", + "categories": [ + "Azure Pipelines" + ], + "targets": [ + { + "id": "Microsoft.VisualStudio.Services" + } + ], + "files": [ + { + "path": "linux-executable", + "addressable": true + }, + { + "path": "README.md", + "addressable": true + } + ] +} From a1adab037839fb005d95781d23d6299eed80fd22 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:56:21 +0000 Subject: [PATCH 3/5] Fix genContentTypesXml to process all package files including extensionless executables Co-authored-by: dpaquette <2531875+dpaquette@users.noreply.github.com> --- .../extension/_lib/vsix-manifest-builder.ts | 54 ++++++------ tests/content-types-test.ts | 87 +++++++++++++++++++ 2 files changed, 114 insertions(+), 27 deletions(-) create mode 100644 tests/content-types-test.ts diff --git a/app/exec/extension/_lib/vsix-manifest-builder.ts b/app/exec/extension/_lib/vsix-manifest-builder.ts index 1127c8ec..ce7cc84e 100644 --- a/app/exec/extension/_lib/vsix-manifest-builder.ts +++ b/app/exec/extension/_lib/vsix-manifest-builder.ts @@ -563,7 +563,7 @@ export class VsixManifestBuilder extends ManifestBuilder { // The vsixmanifest will be responsible for generating the [Content_Types].xml file // Obviously this is kind of strange, but hey ho. - return this.genContentTypesXml(builders).then(result => { + return this.genContentTypesXml(builders, files).then(result => { this.addFile({ path: null, content: result, @@ -585,7 +585,7 @@ export class VsixManifestBuilder extends ManifestBuilder { * This xml contains a entry for each different file extension * found in the package, mapping it to the appropriate MIME type. */ - private genContentTypesXml(builders: ManifestBuilder[]): Promise { + private genContentTypesXml(builders: ManifestBuilder[], packageFiles: PackageFiles): Promise { let typeMap = VsixManifestBuilder.CONTENT_TYPE_MAP; trace.debug("Generating [Content_Types].xml"); let contentTypes: any = { @@ -605,31 +605,31 @@ export class VsixManifestBuilder extends ManifestBuilder { let contentTypePromises: Promise[] = []; let extensionlessFiles = []; let uniqueExtensions = _.uniq( - Object.keys(this.files).map(f => { - let extName = path.extname(f) || path.extname(this.files[f].partName); + Object.keys(packageFiles).map(f => { + let extName = path.extname(f) || path.extname(packageFiles[f].partName); const filename = path.basename(f); // Look in the best guess table. Or, default to text/plain if the file starts with a "." const bestGuess = VsixManifestBuilder.BEST_GUESS_CONTENT_TYPES[filename.toUpperCase()] || (filename[0] === "." ? "text/plain" : null); - if (!extName && !this.files[f].contentType && this.files[f].addressable && !bestGuess) { + if (!extName && !packageFiles[f].contentType && packageFiles[f].addressable && !bestGuess) { trace.warn( "File %s does not have an extension, and its content-type is not declared. Defaulting to application/octet-stream.", path.resolve(f), ); - this.files[f].contentType = "application/octet-stream"; + packageFiles[f].contentType = "application/octet-stream"; } else if (bestGuess) { - this.files[f].contentType = bestGuess; + packageFiles[f].contentType = bestGuess; } - if (this.files[f].contentType) { + if (packageFiles[f].contentType) { // If there is an override for this file, ignore its extension return ""; } // Later, we will show warnings for extensions with unknown content types if there // was at least one file with this extension that was addressable. - if (!showWarningForExtensionMap[extName] && this.files[f].addressable) { + if (!showWarningForExtensionMap[extName] && packageFiles[f].addressable) { showWarningForExtensionMap[extName] = true; } return extName.toLowerCase(); @@ -699,9 +699,9 @@ export class VsixManifestBuilder extends ManifestBuilder { let contentTypePromises: Promise[] = []; let extTypeCounter: { [ext: string]: { [type: string]: string[] } } = {}; - Object.keys(this.files) + Object.keys(packageFiles) .filter(fileName => { - return !this.files[fileName].contentType; + return !packageFiles[fileName].contentType; }) .forEach(fileName => { let extension = path.extname(fileName).toLowerCase(); @@ -721,10 +721,10 @@ export class VsixManifestBuilder extends ManifestBuilder { let child = childProcess.exec('file --mime-type "' + fileName + '"', (err, stdout, stderr) => { try { if (err) { - if (this.files[fileName].addressable) { + if (packageFiles[fileName].addressable) { reject(err); } else { - this.files[fileName].contentType = "application/octet-stream"; + packageFiles[fileName].contentType = "application/octet-stream"; } } else { if (typeof stdout === "string") { @@ -741,25 +741,25 @@ export class VsixManifestBuilder extends ManifestBuilder { } hitCounters[magicMime].push(fileName); } else { - if (!this.files[fileName].contentType) { - this.files[fileName].contentType = magicMime; + if (!packageFiles[fileName].contentType) { + packageFiles[fileName].contentType = magicMime; } } } else { if (stderr) { - if (this.files[fileName].addressable) { + if (packageFiles[fileName].addressable) { reject(stderr); } else { - this.files[fileName].contentType = "application/octet-stream"; + packageFiles[fileName].contentType = "application/octet-stream"; } } else { - if (this.files[fileName].addressable) { + if (packageFiles[fileName].addressable) { trace.warn( "Could not determine content type for %s. Defaulting to application/octet-stream. To override this, add a contentType property to this file entry in the manifest.", fileName, ); } - this.files[fileName].contentType = "application/octet-stream"; + packageFiles[fileName].contentType = "application/octet-stream"; } } } @@ -781,7 +781,7 @@ export class VsixManifestBuilder extends ManifestBuilder { return; } hitCounts[type].forEach(fileName => { - this.files[fileName].contentType = type; + packageFiles[fileName].contentType = type; }); }); contentTypes.Types.Default.push({ @@ -795,25 +795,25 @@ export class VsixManifestBuilder extends ManifestBuilder { } return contentTypePromise.then(() => { let seenPartNames = new Set(); - Object.keys(this.files).forEach(filePath => { - if (this.files[filePath].contentType) { - let partName = "/" + toZipItemName(this.files[filePath].partName); + Object.keys(packageFiles).forEach(filePath => { + if (packageFiles[filePath].contentType) { + let partName = "/" + toZipItemName(packageFiles[filePath].partName); if (!seenPartNames.has(partName)) { contentTypes.Types.Override.push({ $: { - ContentType: this.files[filePath].contentType, + ContentType: packageFiles[filePath].contentType, PartName: partName, }, }); seenPartNames.add(partName); } - if ((this.files[filePath] as any)._additionalPackagePaths) { - for (const additionalPath of (this.files[filePath] as any)._additionalPackagePaths) { + if ((packageFiles[filePath] as any)._additionalPackagePaths) { + for (const additionalPath of (packageFiles[filePath] as any)._additionalPackagePaths) { let additionalPartName = "/" + toZipItemName(additionalPath); if (!seenPartNames.has(additionalPartName)) { contentTypes.Types.Override.push({ $: { - ContentType: this.files[filePath].contentType, + ContentType: packageFiles[filePath].contentType, PartName: additionalPartName, }, }); diff --git a/tests/content-types-test.ts b/tests/content-types-test.ts new file mode 100644 index 00000000..2bfccfad --- /dev/null +++ b/tests/content-types-test.ts @@ -0,0 +1,87 @@ +import assert = require('assert'); +import path = require('path'); +import fs = require('fs'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const AdmZip = require('adm-zip'); +import { execAsyncWithLogging } from './test-utils/debug-exec'; + +// Basic test framework functions +declare function describe(name: string, fn: Function): void; +declare function it(name: string, fn: Function): void; +declare function after(fn: Function): void; + +const tfxPath = path.resolve(__dirname, '../../_build/tfx-cli.js'); +const samplesPath = path.resolve(__dirname, '../extension-samples'); + +describe('Content_Types.xml Generation', function() { + this.timeout(30000); + + after(function() { + // cleanup - remove generated .vsix files + const testExtensionPath = path.join(samplesPath, 'extensionless-exec-test'); + const testVsixPath = path.join(testExtensionPath, 'test.vsix'); + try { + if (fs.existsSync(testVsixPath)) { + fs.unlinkSync(testVsixPath); + } + } catch (e) { + // Ignore cleanup errors + } + }); + + it('should include extensionless executable in Content_Types.xml Override section', function(done) { + const testExtensionPath = path.join(samplesPath, 'extensionless-exec-test'); + const outputPath = path.join(testExtensionPath, 'test.vsix'); + + // Verify the test extension exists + if (!fs.existsSync(testExtensionPath)) { + done(new Error('Test extension directory not found: ' + testExtensionPath)); + return; + } + + execAsyncWithLogging(`node "${tfxPath}" extension create --root "${testExtensionPath}" --output-path "${outputPath}"`, 'extension create with extensionless executable') + .then(({ stdout }) => { + // Verify .vsix was created + assert(fs.existsSync(outputPath), 'Should create .vsix file'); + + // Extract and check Content_Types.xml + const zip = new AdmZip(outputPath); + const zipEntries = zip.getEntries(); + + // Find Content_Types.xml + let contentTypesEntry = null; + for (const entry of zipEntries) { + if (entry.entryName === '[Content_Types].xml') { + contentTypesEntry = entry; + break; + } + } + + assert(contentTypesEntry, 'Should contain [Content_Types].xml'); + + // Read and parse Content_Types.xml + const contentTypesXml = contentTypesEntry.getData().toString('utf8'); + + // Check that linux-executable is included as an Override element + // The file should be listed with its full path + assert( + contentTypesXml.includes('linux-executable') || contentTypesXml.includes('Override'), + 'Content_Types.xml should contain Override elements for extensionless files' + ); + + // More specific check: look for the Override element with linux-executable + const hasOverrideForExecutable = + contentTypesXml.includes('PartName="/linux-executable"') || + contentTypesXml.includes('PartName="/linux-executable') || + contentTypesXml.includes('linux-executable"'); + + assert( + hasOverrideForExecutable, + 'Content_Types.xml should have Override element for linux-executable. Content: ' + contentTypesXml + ); + + done(); + }) + .catch(done); + }); +}); From 22ac6328cde56da50d4d6423589d7c5bd0d8e68f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 16:01:12 +0000 Subject: [PATCH 4/5] Add detailed logging to content types test Co-authored-by: dpaquette <2531875+dpaquette@users.noreply.github.com> --- tests/content-types-test.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/content-types-test.ts b/tests/content-types-test.ts index 2bfccfad..1b575187 100644 --- a/tests/content-types-test.ts +++ b/tests/content-types-test.ts @@ -61,6 +61,7 @@ describe('Content_Types.xml Generation', function() { // Read and parse Content_Types.xml const contentTypesXml = contentTypesEntry.getData().toString('utf8'); + console.log('Content_Types.xml content:\n', contentTypesXml); // Check that linux-executable is included as an Override element // The file should be listed with its full path @@ -70,16 +71,26 @@ describe('Content_Types.xml Generation', function() { ); // More specific check: look for the Override element with linux-executable + // The path should match the file location in the package const hasOverrideForExecutable = contentTypesXml.includes('PartName="/linux-executable"') || contentTypesXml.includes('PartName="/linux-executable') || - contentTypesXml.includes('linux-executable"'); + contentTypesXml.includes('linux-executable"') || + contentTypesXml.match(/Override.*linux-executable/); assert( hasOverrideForExecutable, - 'Content_Types.xml should have Override element for linux-executable. Content: ' + contentTypesXml + 'Content_Types.xml should have Override element for linux-executable.\nContent: ' + contentTypesXml ); + // Verify that the Override element has a ContentType attribute + if (contentTypesXml.includes('linux-executable')) { + assert( + contentTypesXml.match(/ContentType="[^"]+"/), + 'Override element should have ContentType attribute' + ); + } + done(); }) .catch(done); From 93a7387e398c9649d79d383297946f189e1a26f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Dec 2025 16:03:54 +0000 Subject: [PATCH 5/5] Simplify test assertion logic per code review feedback Co-authored-by: dpaquette <2531875+dpaquette@users.noreply.github.com> --- tests/content-types-test.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/content-types-test.ts b/tests/content-types-test.ts index 1b575187..cedb9b22 100644 --- a/tests/content-types-test.ts +++ b/tests/content-types-test.ts @@ -72,15 +72,12 @@ describe('Content_Types.xml Generation', function() { // More specific check: look for the Override element with linux-executable // The path should match the file location in the package - const hasOverrideForExecutable = - contentTypesXml.includes('PartName="/linux-executable"') || - contentTypesXml.includes('PartName="/linux-executable') || - contentTypesXml.includes('linux-executable"') || - contentTypesXml.match(/Override.*linux-executable/); + const overridePattern = /Override.*PartName="[^"]*linux-executable[^"]*"/; + const hasOverrideForExecutable = overridePattern.test(contentTypesXml); assert( hasOverrideForExecutable, - 'Content_Types.xml should have Override element for linux-executable.\nContent: ' + contentTypesXml + 'Content_Types.xml should have Override element with PartName containing linux-executable.\nContent: ' + contentTypesXml ); // Verify that the Override element has a ContentType attribute