diff --git a/extensions/sql-database-projects/src/common/constants.ts b/extensions/sql-database-projects/src/common/constants.ts index 7b27a300a4..9f014315b2 100644 --- a/extensions/sql-database-projects/src/common/constants.ts +++ b/extensions/sql-database-projects/src/common/constants.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vscode-nls'; +import * as path from 'path'; import { SqlTargetPlatform } from 'sqldbproj'; import * as utils from '../common/utils'; @@ -457,6 +458,10 @@ export const Sdk: string = 'Sdk'; export const DatabaseSource = 'DatabaseSource'; export const VisualStudioVersion = 'VisualStudioVersion'; export const SSDTExists = 'SSDTExists'; +export const OutputPath = 'OutputPath'; +export const Configuration = 'Configuration'; +export const Platform = 'Platform'; +export const AnyCPU = 'AnyCPU'; export const BuildElements = localize('buildElements', "Build Elements"); export const FolderElements = localize('folderElements', "Folder Elements"); @@ -494,6 +499,11 @@ export const RoundTripSqlDbPresentCondition = '\'$(NetCoreBuild)\' != \'true\' A export const RoundTripSqlDbNotPresentCondition = '\'$(NetCoreBuild)\' != \'true\' AND \'$(SQLDBExtensionsRefPath)\' == \'\''; export const DacpacRootPath = '$(DacPacRootPath)'; export const ProjJsonToClean = '$(BaseIntermediateOutputPath)\\project.assets.json'; +export const EmptyConfigurationCondition = '\'$(Configuration)\' == \'\''; +export const EmptyPlatformCondition = '\'$(Platform)\' == \'\''; +export function ConfigurationPlatformCondition(configuration: string, platform: string) { return `'$(Configuration)|$(Platform)' == '${configuration}|${platform}'`; } + +export function defaultOutputPath(configuration: string) { return path.join('.', 'bin', configuration); } // Sqlproj VS property conditions export const VSVersionCondition = '\'$(VisualStudioVersion)\' == \'\''; diff --git a/extensions/sql-database-projects/src/models/project.ts b/extensions/sql-database-projects/src/models/project.ts index ce317aa992..03575f1090 100644 --- a/extensions/sql-database-projects/src/models/project.ts +++ b/extensions/sql-database-projects/src/models/project.ts @@ -20,6 +20,15 @@ import { ISystemDatabaseReferenceSettings, IDacpacReferenceSettings, IProjectRef import { TelemetryActions, TelemetryReporter, TelemetryViews } from '../common/telemetry'; import { DacpacReferenceProjectEntry, FileProjectEntry, ProjectEntry, SqlCmdVariableProjectEntry, SqlProjectReferenceProjectEntry, SystemDatabase, SystemDatabaseReferenceProjectEntry } from './projectEntry'; +/** + * Represents the configuration based on the Configuration property in the sqlproj + */ +enum Configuration { + Debug = 'Debug', // default used if the Configuration property is not specified + Release = 'Release', + Output = 'Output' // if a string besides debug or release is used, then Output is used as the configuration +} + /** * Class representing a Project, and providing functions for operating on it */ @@ -36,9 +45,11 @@ export class Project implements ISqlProject { private _postDeployScripts: FileProjectEntry[] = []; private _noneDeployScripts: FileProjectEntry[] = []; private _isSdkStyleProject: boolean = false; // https://docs.microsoft.com/en-us/dotnet/core/project-sdk/overview + private _outputPath: string = ''; + private _configuration: Configuration = Configuration.Debug; public get dacpacOutputPath(): string { - return path.join(this.projectFolderPath, 'bin', 'Debug', `${this._projectFileName}.dacpac`); + return path.join(this.outputPath, `${this._projectFileName}.dacpac`); } public get projectFolderPath() { @@ -93,6 +104,14 @@ export class Project implements ISqlProject { return this._isSdkStyleProject; } + public get outputPath(): string { + return this._outputPath; + } + + public get configuration(): Configuration { + return this._configuration; + } + private projFileXmlDoc: Document | undefined = undefined; constructor(projectFilePath: string) { @@ -155,6 +174,67 @@ export class Project implements ISqlProject { this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.PropertyGroup)[0]?.appendChild(newProjectGuidNode); await this.serializeToProjFile(this.projFileXmlDoc); } + + // get configuration + const configurationNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.Configuration); + if (configurationNodes.length > 0) { + const configuration = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.Configuration)[0].childNodes[0].nodeValue!; + switch (configuration.toLowerCase()) { + case Configuration.Debug.toString().toLowerCase(): + this._configuration = Configuration.Debug; + break; + case Configuration.Release.toString().toLowerCase(): + this._configuration = Configuration.Release; + break; + default: + // if the configuration doesn't match release or debug, the dacpac will get created in ./bin/Output + this._configuration = Configuration.Output; + } + } else { + // If configuration isn't specified in .sqlproj, set it to the default debug + this._configuration = Configuration.Debug; + } + + // get platform + const platformNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.Platform); + let platform = ''; + if (platformNodes.length > 0) { + for (let i = 0; i < platformNodes.length; i++) { + const condition = platformNodes[i].getAttribute(constants.Condition); + if (condition?.trim() === constants.EmptyPlatformCondition.trim()) { + platform = platformNodes[i].childNodes[0].nodeValue ?? ''; + break; + } + } + } else { + platform = constants.AnyCPU; + } + + // get output path + let outputPath; + const outputPathNodes = this.projFileXmlDoc!.documentElement.getElementsByTagName(constants.OutputPath); + if (outputPathNodes.length > 0) { + // go through all the OutputPath nodes and use the last one in the .sqlproj that the condition matches + for (let i = 0; i < outputPathNodes.length; i++) { + // check if parent has a condition + const parent = outputPathNodes[i].parentNode as Element; + const condition = parent?.getAttribute(constants.Condition); + + // only handle the default conditions format that are there when creating a sqlproj in VS or ADS + if (condition?.toLowerCase().trim() === constants.ConfigurationPlatformCondition(this.configuration.toString(), platform).toLowerCase()) { + outputPath = outputPathNodes[i].childNodes[0].nodeValue; + } else if (!condition) { + outputPath = outputPathNodes[i].childNodes[0].nodeValue; + } + } + } + + if (outputPath) { + this._outputPath = path.join(utils.getPlatformSafeFileEntryPath(this.projectFolderPath), utils.getPlatformSafeFileEntryPath(outputPath)); + } else { + // If output path isn't specified in .sqlproj, set it to the default output path .\bin\Debug\ + this._outputPath = path.join(utils.getPlatformSafeFileEntryPath(this.projectFolderPath), utils.getPlatformSafeFileEntryPath(constants.defaultOutputPath(this.configuration.toString()))); + } } /** @@ -539,6 +619,8 @@ export class Project implements ISqlProject { this._postDeployScripts = []; this._noneDeployScripts = []; this.projFileXmlDoc = undefined; + this._outputPath = ''; + this._configuration = Configuration.Debug; } /** diff --git a/extensions/sql-database-projects/src/test/baselines/baselines.ts b/extensions/sql-database-projects/src/test/baselines/baselines.ts index 73cf5e1786..b0a1da1d8c 100644 --- a/extensions/sql-database-projects/src/test/baselines/baselines.ts +++ b/extensions/sql-database-projects/src/test/baselines/baselines.ts @@ -11,6 +11,10 @@ export let newProjectFileBaseline: string; export let newProjectFileWithScriptBaseline: string; export let newProjectFileNoPropertiesFolderBaseline: string; export let openProjectFileBaseline: string; +export let openProjectFileReleaseConfigurationBaseline: string; +export let openProjectFileUnknownConfigurationBaseline: string; +export let openProjectFileSingleOutputPathBaseline: string; +export let openProjectFileMultipleOutputPathBaseline: string; export let openDataSourcesBaseline: string; export let SSDTProjectFileBaseline: string; export let SSDTProjectAfterUpdateBaseline: string; @@ -44,6 +48,10 @@ export async function loadBaselines() { newProjectFileWithScriptBaseline = await loadBaseline(baselineFolderPath, 'newSqlProjectWithScriptBaseline.xml'); newProjectFileNoPropertiesFolderBaseline = await loadBaseline(baselineFolderPath, 'newSqlProjectNoPropertiesFolderBaseline.xml'); openProjectFileBaseline = await loadBaseline(baselineFolderPath, 'openSqlProjectBaseline.xml'); + openProjectFileReleaseConfigurationBaseline = await loadBaseline(baselineFolderPath, 'openSqlProjectReleaseConfigurationBaseline.xml'); + openProjectFileUnknownConfigurationBaseline = await loadBaseline(baselineFolderPath, 'openSqlProjectUnknownConfigurationBaseline.xml'); + openProjectFileSingleOutputPathBaseline = await loadBaseline(baselineFolderPath, 'openSqlProjectSingleOutputPathBaseline.xml'); + openProjectFileMultipleOutputPathBaseline = await loadBaseline(baselineFolderPath, 'openSqlProjectMultipleOutputPathBaseline.xml'); openDataSourcesBaseline = await loadBaseline(baselineFolderPath, 'openDataSourcesBaseline.json'); SSDTProjectFileBaseline = await loadBaseline(baselineFolderPath, 'SSDTProjectBaseline.xml'); SSDTProjectAfterUpdateBaseline = await loadBaseline(baselineFolderPath, 'SSDTProjectAfterUpdateBaseline.xml'); diff --git a/extensions/sql-database-projects/src/test/baselines/openSdkStyleSqlProjectBaseline.xml b/extensions/sql-database-projects/src/test/baselines/openSdkStyleSqlProjectBaseline.xml index dbf2406a1a..a956694190 100644 --- a/extensions/sql-database-projects/src/test/baselines/openSdkStyleSqlProjectBaseline.xml +++ b/extensions/sql-database-projects/src/test/baselines/openSdkStyleSqlProjectBaseline.xml @@ -6,6 +6,7 @@ {2C283C5D-9E4A-4313-8FF9-4E0CEE20B063} Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider 1033, CI + ..\otherFolder diff --git a/extensions/sql-database-projects/src/test/baselines/openSqlProjectMultipleOutputPathBaseline.xml b/extensions/sql-database-projects/src/test/baselines/openSqlProjectMultipleOutputPathBaseline.xml new file mode 100644 index 0000000000..78bcb8405a --- /dev/null +++ b/extensions/sql-database-projects/src/test/baselines/openSqlProjectMultipleOutputPathBaseline.xml @@ -0,0 +1,114 @@ + + + + Debug + AnyCPU + TestProjectName + 2.0 + 4.1 + {BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575} + Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider + Database + + + TestProjectName + TestProjectName + 1033, CI + BySchemaAndSchemaType + True + v4.5 + CS + Properties + False + True + True + + + bin\Release\ + $(MSBuildProjectName).sql + False + pdbonly + true + false + true + prompt + 4 + + + bin\Debug\ + $(MSBuildProjectName).sql + false + true + full + false + true + true + prompt + 4 + + + bin\other + + + 11.0 + + True + 11.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MyProdDatabase + $(SqlCmdVar__1) + + + MyBackupDatabase + $(SqlCmdVar__2) + + + + + False + master + + + False + master + + + + + + + + + + + + + + diff --git a/extensions/sql-database-projects/src/test/baselines/openSqlProjectReleaseConfigurationBaseline.xml b/extensions/sql-database-projects/src/test/baselines/openSqlProjectReleaseConfigurationBaseline.xml new file mode 100644 index 0000000000..19324c0b31 --- /dev/null +++ b/extensions/sql-database-projects/src/test/baselines/openSqlProjectReleaseConfigurationBaseline.xml @@ -0,0 +1,111 @@ + + + + Release + AnyCPU + TestProjectName + 2.0 + 4.1 + {BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575} + Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider + Database + + + TestProjectName + TestProjectName + 1033, CI + BySchemaAndSchemaType + True + v4.5 + CS + Properties + False + True + True + + + bin\Release\ + $(MSBuildProjectName).sql + False + pdbonly + true + false + true + prompt + 4 + + + bin\Debug\ + $(MSBuildProjectName).sql + false + true + full + false + true + true + prompt + 4 + + + 11.0 + + True + 11.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MyProdDatabase + $(SqlCmdVar__1) + + + MyBackupDatabase + $(SqlCmdVar__2) + + + + + False + master + + + False + master + + + + + + + + + + + + + + diff --git a/extensions/sql-database-projects/src/test/baselines/openSqlProjectSingleOutputPathBaseline.xml b/extensions/sql-database-projects/src/test/baselines/openSqlProjectSingleOutputPathBaseline.xml new file mode 100644 index 0000000000..a8587678d2 --- /dev/null +++ b/extensions/sql-database-projects/src/test/baselines/openSqlProjectSingleOutputPathBaseline.xml @@ -0,0 +1,89 @@ + + + + Debug + AnyCPU + TestProjectName + 2.0 + 4.1 + {BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575} + Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider + Database + + + TestProjectName + TestProjectName + 1033, CI + BySchemaAndSchemaType + True + v4.5 + CS + Properties + False + True + True + ..\otherFolder + + + 11.0 + + True + 11.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MyProdDatabase + $(SqlCmdVar__1) + + + MyBackupDatabase + $(SqlCmdVar__2) + + + + + False + master + + + False + master + + + + + + + + + + + + + + diff --git a/extensions/sql-database-projects/src/test/baselines/openSqlProjectUnknownConfigurationBaseline.xml b/extensions/sql-database-projects/src/test/baselines/openSqlProjectUnknownConfigurationBaseline.xml new file mode 100644 index 0000000000..3bd55a2436 --- /dev/null +++ b/extensions/sql-database-projects/src/test/baselines/openSqlProjectUnknownConfigurationBaseline.xml @@ -0,0 +1,111 @@ + + + + Unknown + AnyCPU + TestProjectName + 2.0 + 4.1 + {BA5EBA11-C0DE-5EA7-ACED-BABB1E70A575} + Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider + Database + + + TestProjectName + TestProjectName + 1033, CI + BySchemaAndSchemaType + True + v4.5 + CS + Properties + False + True + True + + + bin\Release\ + $(MSBuildProjectName).sql + False + pdbonly + true + false + true + prompt + 4 + + + bin\Debug\ + $(MSBuildProjectName).sql + false + true + full + false + true + true + prompt + 4 + + + 11.0 + + True + 11.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MyProdDatabase + $(SqlCmdVar__1) + + + MyBackupDatabase + $(SqlCmdVar__2) + + + + + False + master + + + False + master + + + + + + + + + + + + + + diff --git a/extensions/sql-database-projects/src/test/project.test.ts b/extensions/sql-database-projects/src/test/project.test.ts index 69d0650a78..fadbcafc6a 100644 --- a/extensions/sql-database-projects/src/test/project.test.ts +++ b/extensions/sql-database-projects/src/test/project.test.ts @@ -13,7 +13,7 @@ import * as constants from '../common/constants'; import { promises as fs } from 'fs'; import { Project } from '../models/project'; -import { exists, convertSlashesForSqlProj, getWellKnownDatabaseSources } from '../common/utils'; +import { exists, convertSlashesForSqlProj, getWellKnownDatabaseSources, getPlatformSafeFileEntryPath } from '../common/utils'; import { Uri, window } from 'vscode'; import { IDacpacReferenceSettings, IProjectReferenceSettings, ISystemDatabaseReferenceSettings } from '../models/IDatabaseReferenceSettings'; import { EntryType, ItemType, SqlTargetPlatform } from 'sqldbproj'; @@ -858,6 +858,51 @@ describe('Project: sqlproj content operations', function (): void { should(projFileText.includes('')).equal(true, projFileText); should(projFileText.includes('')).equal(true, projFileText); }); + + it('Should read OutputPath from sqlproj if there is one for legacy-style project with Debug configuration', async function (): Promise { + projFilePath = await testUtils.createTestSqlProjFile(baselines.openProjectFileBaseline); + const project: Project = await Project.openProject(projFilePath); + + should(project.configuration).equal('Debug'); + should(project.outputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('bin\\Debug\\'))); + should(project.dacpacOutputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('bin\\Debug\\'), `${project.projectFileName}.dacpac`)); + }); + + it('Should read OutputPath from sqlproj if there is one for legacy-style project with Release configuration', async function (): Promise { + projFilePath = await testUtils.createTestSqlProjFile(baselines.openProjectFileReleaseConfigurationBaseline); + const project: Project = await Project.openProject(projFilePath); + + should(project.configuration).equal('Release'); + should(project.outputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('bin\\Release\\'))); + should(project.dacpacOutputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('bin\\Release\\'), `${project.projectFileName}.dacpac`)); + }); + + it('Should set configuration to Output for legacy-style project with unknown configuration', async function (): Promise { + projFilePath = await testUtils.createTestSqlProjFile(baselines.openProjectFileUnknownConfigurationBaseline); + const project: Project = await Project.openProject(projFilePath); + + should(project.configuration).equal('Output'); + should(project.outputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('bin\\Output'))); + should(project.dacpacOutputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('bin\\Output\\'), `${project.projectFileName}.dacpac`)); + }); + + it('Should set configuration to Output for legacy-style project with unknown configuration', async function (): Promise { + projFilePath = await testUtils.createTestSqlProjFile(baselines.openProjectFileSingleOutputPathBaseline); + const project: Project = await Project.openProject(projFilePath); + + should(project.configuration).equal('Debug'); + should(project.outputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('..\\otherFolder'))); + should(project.dacpacOutputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('..\\otherFolder'), `${project.projectFileName}.dacpac`)); + }); + + it('Should use the last OutputPath in the .sqlproj that matches the conditions', async function (): Promise { + projFilePath = await testUtils.createTestSqlProjFile(baselines.openProjectFileMultipleOutputPathBaseline); + const project: Project = await Project.openProject(projFilePath); + + should(project.configuration).equal('Debug'); + should(project.outputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('bin\\other'))); + should(project.dacpacOutputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('bin\\other'), `${project.projectFileName}.dacpac`)); + }); }); describe('Project: sdk style project content operations', function (): void { @@ -1430,6 +1475,30 @@ describe('Project: sdk style project content operations', function (): void { should(projFileText.includes(constants.ProjectGuid)).equal(true); }); + it('Should read OutputPath from sqlproj if there is one for SDK-style project', async function (): Promise { + projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectBaseline); + const projFileText = (await fs.readFile(projFilePath)).toString(); + + // Verify sqlproj has OutputPath + should(projFileText.includes(constants.OutputPath)).equal(true); + + const project: Project = await Project.openProject(projFilePath); + should(project.outputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('..\\otherFolder'))); + should(project.dacpacOutputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath('..\\otherFolder'), `${project.projectFileName}.dacpac`)); + }); + + it('Should use default output path if OutputPath is not specified in sqlproj', async function (): Promise { + projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectWithGlobsSpecifiedBaseline); + const projFileText = (await fs.readFile(projFilePath)).toString(); + + // Verify sqlproj doesn't have OutputPath + should(projFileText.includes(constants.OutputPath)).equal(true); + + const project: Project = await Project.openProject(projFilePath); + should(project.outputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath(constants.defaultOutputPath(project.configuration.toString())))); + should(project.dacpacOutputPath).equal(path.join(getPlatformSafeFileEntryPath(project.projectFolderPath), getPlatformSafeFileEntryPath(constants.defaultOutputPath(project.configuration.toString())), `${project.projectFileName}.dacpac`)); + }); + it('Should handle adding existing items to project', async function (): Promise { projFilePath = await testUtils.createTestSqlProjFile(baselines.openSdkStyleSqlProjectBaseline); const projectFolder = path.dirname(projFilePath);