Feature/project build (#10332)

* initial build command execution

* adding tests

* Clean up test names

* update SqltoolsService release in ADS for Build

* Updating as per PR comments

* updating yarn lock

* Adding one more test for command run

* Test fixes
This commit is contained in:
Udeesha Gautam
2020-05-12 20:17:37 -07:00
committed by GitHub
parent ab374e362a
commit ddb9a806cc
13 changed files with 319 additions and 22 deletions

View File

@@ -106,6 +106,7 @@ const indentationFilter = [
'!extensions/sql-database-projects/resources/templates/*.xml',
'!extensions/sql-database-projects/src/test/baselines/*.xml',
'!extensions/sql-database-projects/src/test/baselines/*.json',
'!extensions/sql-database-projects/src/test/baselines/*.sqlproj',
'!extensions/big-data-cluster/src/bigDataCluster/controller/apiGenerated.ts',
'!extensions/big-data-cluster/src/bigDataCluster/controller/clusterApiGenerated2.ts',
'!resources/linux/snap/electron-launch'

View File

@@ -1,6 +1,6 @@
{
"downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}",
"version": "2.0.0-release.64",
"version": "2.0.0-release.65",
"downloadFileNames": {
"Windows_86": "win-x86-netcoreapp2.2.zip",
"Windows_64": "win-x64-netcoreapp2.2.zip",

View File

@@ -1 +1,2 @@
*.vsix
*.vsix
BuildDirectory/*.*

View File

@@ -7,7 +7,7 @@
"preview": true,
"engines": {
"vscode": "^1.30.1",
"azdata": ">=1.12.0"
"azdata": ">=1.18.0"
},
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt",
"icon": "images/extension.png",
@@ -239,6 +239,7 @@
}
},
"dependencies": {
"promisify-child-process": "^3.1.1",
"vscode-languageclient": "^5.3.0-next.1",
"vscode-nls": "^3.2.1",
"xmldom": "^0.3.0"

View File

@@ -52,7 +52,7 @@ export const projectNameRequired = localize('projectNameRequired', "Name is requ
export const projectLocationRequired = localize('projectLocationRequired', "Location is required to create a new database project.");
export function projectAlreadyOpened(path: string) { return localize('projectAlreadyOpened', "Project '{0}' is already opened.", path); }
export function projectAlreadyExists(name: string, path: string) { return localize('projectAlreadyExists', "A project named {0} already exists in {1}.", name, path); }
export function mssqlNotFound(mssqlConfigDir: string) { return localize('mssqlNotFound', "Could not get mssql extension's install location at {0}", mssqlConfigDir); }
// Project script types

View File

@@ -4,11 +4,14 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as os from 'os';
/**
* Consolidates on the error message string
*/
export function getErrorMessage(error: Error | string): string {
return (error instanceof Error) ? error.message : error;
export function getErrorMessage(error: any): string {
return (error instanceof Error)
? (typeof error.message === 'string' ? error.message : '')
: typeof error === 'string' ? error : `${JSON.stringify(error, undefined, '\t')}`;
}
/**
@@ -45,3 +48,29 @@ export function trimChars(input: string, chars: string): string {
return output;
}
/**
* get quoted path to be used in any commandline argument
* @param filePath
*/
export function getSafePath(filePath: string): string {
return (os.platform() === 'win32') ?
getSafeWindowsPath(filePath) :
getSafeNonWindowsPath(filePath);
}
/**
* ensure that path with spaces are handles correctly
*/
export function getSafeWindowsPath(filePath: string): string {
filePath = filePath.split('\\').join('\\\\').split('"').join('');
return '"' + filePath + '"';
}
/**
* ensure that path with spaces are handles correctly
*/
export function getSafeNonWindowsPath(filePath: string): string {
filePath = filePath.split('\\').join('/').split('"').join('');
return '"' + filePath + '"';
}

View File

@@ -49,8 +49,8 @@ export default class MainController implements Disposable {
this.apiWrapper.registerCommand('sqlDatabaseProjects.close', (node: BaseProjectTreeItem) => { this.projectsController.closeProject(node); });
this.apiWrapper.registerCommand('sqlDatabaseProjects.properties', async (node: BaseProjectTreeItem) => { await this.apiWrapper.showErrorMessage(`Properties not yet implemented: ${node.uri.path}`); }); // TODO
this.apiWrapper.registerCommand('sqlDatabaseProjects.build', async (node: BaseProjectTreeItem) => { await this.projectsController.build(node); });
this.apiWrapper.registerCommand('sqlDatabaseProjects.deploy', (node: BaseProjectTreeItem) => { this.projectsController.deploy(node); });
this.apiWrapper.registerCommand('sqlDatabaseProjects.build', async (node: BaseProjectTreeItem) => { await this.projectsController.buildProject(node); });
this.apiWrapper.registerCommand('sqlDatabaseProjects.deploy', async (node: BaseProjectTreeItem) => { await this.projectsController.deploy(node); });
this.apiWrapper.registerCommand('sqlDatabaseProjects.import', async (node: BaseProjectTreeItem) => { await this.projectsController.import(node); });
this.apiWrapper.registerCommand('sqlDatabaseProjects.newScript', async (node: BaseProjectTreeItem) => { await this.projectsController.addItemPromptFromNode(node, templates.script); });

View File

@@ -19,20 +19,25 @@ import { BaseProjectTreeItem } from '../models/tree/baseTreeItem';
import { ProjectRootTreeItem } from '../models/tree/projectTreeItem';
import { FolderNode } from '../models/tree/fileFolderTreeItem';
import { DeployDatabaseDialog } from '../dialogs/deployDatabaseDialog';
import { NetCoreTool, DotNetCommandOptions } from '../tools/netcoreTool';
import { BuildHelper } from '../tools/buildHelper';
/**
* Controller for managing project lifecycle
*/
export class ProjectsController {
private projectTreeViewProvider: SqlDatabaseProjectTreeViewProvider;
private netCoreTool: NetCoreTool;
private buildHelper: BuildHelper;
projects: Project[] = [];
constructor(private apiWrapper: ApiWrapper, projTreeViewProvider: SqlDatabaseProjectTreeViewProvider) {
this.projectTreeViewProvider = projTreeViewProvider;
this.netCoreTool = new NetCoreTool();
this.buildHelper = new BuildHelper();
}
public refreshProjectsTree() {
this.projectTreeViewProvider.load(this.projects);
}
@@ -114,9 +119,17 @@ export class ProjectsController {
this.refreshProjectsTree();
}
public async build(treeNode: BaseProjectTreeItem) {
public async buildProject(treeNode: BaseProjectTreeItem): Promise<void> {
// Check mssql extension for project dlls (tracking issue #10273)
await this.buildHelper.createBuildDirFolder();
const project = this.getProjectContextFromTreeNode(treeNode);
await this.apiWrapper.showErrorMessage(`Build not yet implemented: ${project.projectFilePath}`); // TODO
const options: DotNetCommandOptions = {
commandTitle: 'Build',
workingDirectory: project.projectFolderPath,
argument: this.buildHelper.constructBuildArguments(project.projectFilePath, this.buildHelper.extensionBuildDirPath)
};
await this.netCoreTool.runDotnetCommand(options);
}
public deploy(treeNode: BaseProjectTreeItem): void {

View File

@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as should from 'should';
import * as os from 'os';
import * as vscode from 'vscode';
import * as path from 'path';
import { BuildHelper } from '../tools/buildHelper';
describe('BuildHelper: Build Helper tests', function (): void {
it('Should get correct build arguments', async function (): Promise<void> {
// update settings and validate
const buildHelper = new BuildHelper();
const resultArg = buildHelper.constructBuildArguments('dummy\\project path\\more space in path', 'dummy\\dll path');
if (os.platform() === 'win32') {
should(resultArg).equal(' build "dummy\\\\project path\\\\more space in path" /p:NetCoreBuild=true /p:NETCoreTargetsPath="dummy\\\\dll path"');
}
else {
should(resultArg).equal(' build "dummy/project path/more space in path" /p:NetCoreBuild=true /p:NETCoreTargetsPath="dummy/dll path"');
}
});
it('Should get correct build folder', async function (): Promise<void> {
const buildHelper = new BuildHelper();
await buildHelper.createBuildDirFolder();
// get expected path for build
let expectedPath = vscode.extensions.getExtension('Microsoft.sql-database-projects')?.extensionPath ?? 'EmptyPath';
expectedPath = path.join(expectedPath, 'BuildDirectory');
should(buildHelper.extensionBuildDirPath).equal(expectedPath);
});
});

View File

@@ -5,19 +5,23 @@
import * as should from 'should';
import * as os from 'os';
import * as fs from 'fs';
import * as path from 'path';
import * as vscode from 'vscode';
import { NetCoreTool, DBProjectConfigurationKey, NetCoreInstallLocationKey, NextCoreNonWindowsDefaultPath } from '../tools/netcoreTool';
import { getSafePath } from '../common/utils';
import { isNullOrUndefined } from 'util';
import { generateTestFolderPath } from './testUtils';
describe('NetCoreTool: Net core install popup tests', function (): void {
describe('NetCoreTool: Net core tests', function (): void {
it('settings value should override default paths', async function (): Promise<void> {
it('Should override dotnet default value with settings', async function (): Promise<void> {
try {
// update settings and validate
await vscode.workspace.getConfiguration(DBProjectConfigurationKey).update(NetCoreInstallLocationKey, 'test value path', true);
const netcoreTool = new NetCoreTool();
should(netcoreTool.netcoreInstallLocation).equal('test value path'); // the path in settings should be taken
should(netcoreTool.isNetCoreInstallationPresent).equal(false); // dotnet can not be present at dummy path in settings
should(netcoreTool.findOrInstallNetCore()).equal(false); // dotnet can not be present at dummy path in settings
}
finally {
// clean again
@@ -25,7 +29,7 @@ describe('NetCoreTool: Net core install popup tests', function (): void {
}
});
it('should find right default paths', async function (): Promise<void> {
it('Should find right dotnet default paths', async function (): Promise<void> {
const netcoreTool = new NetCoreTool();
netcoreTool.findOrInstallNetCore();
@@ -41,8 +45,23 @@ describe('NetCoreTool: Net core install popup tests', function (): void {
should(result).true('dotnet is either not present or in /usr/local/share by default');
}
});
});
export async function sleep(ms: number): Promise<{}> {
return new Promise(resolve => setTimeout(resolve, ms));
}
it('should run a command successfully', async function (): Promise<void> {
const netcoreTool = new NetCoreTool();
const dummyFile = path.join(await generateTestFolderPath(), 'dummy.dacpac');
const outputChannel = vscode.window.createOutputChannel('db project test');
try {
await netcoreTool.runStreamedCommand('echo test > ' + getSafePath(dummyFile), outputChannel, undefined);
const text = await fs.promises.readFile(dummyFile);
should(text.toString().trim()).equal('test');
}
finally {
await fs.exists(dummyFile, async (existBool) => {
if (existBool) {
await fs.promises.unlink(dummyFile);
}
});
}
});
});

View File

@@ -0,0 +1,93 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as path from 'path';
import * as os from 'os';
import { promises as fs, existsSync } from 'fs';
import * as utils from '../common/utils';
import { mssqlNotFound } from '../common/constants';
const buildDirectory = 'BuildDirectory';
const buildFiles: string[] = [
'Microsoft.Data.SqlClient.dll',
'Microsoft.Data.Tools.Schema.Sql.dll',
'Microsoft.Data.Tools.Schema.Tasks.Sql.dll',
'Microsoft.Data.Tools.Utilities.dll',
'Microsoft.SqlServer.Dac.dll',
'Microsoft.SqlServer.Dac.Extensions.dll',
'Microsoft.SqlServer.TransactSql.ScriptDom.dll',
'Microsoft.SqlServer.Types.dll',
'System.ComponentModel.Composition.dll',
'Microsoft.Data.Tools.Schema.SqlTasks.targets'
];
export class BuildHelper {
private extensionDir: string;
private extensionBuildDir: string;
private initialized: boolean = false;
constructor() {
this.extensionDir = vscode.extensions.getExtension('Microsoft.sql-database-projects')?.extensionPath ?? '';
this.extensionBuildDir = path.join(this.extensionDir, buildDirectory);
}
// create build dlls directory
// this should not be required. temporary solution for issue #10273
public async createBuildDirFolder(): Promise<void> {
if (this.initialized) {
return;
}
if (!existsSync(this.extensionBuildDir)) {
await fs.mkdir(this.extensionBuildDir);
}
const buildfilesPath = await this.getBuildDirPathFromMssqlTools();
buildFiles.forEach(async (fileName) => {
if (existsSync(path.join(buildfilesPath, fileName))) {
await fs.copyFile(path.join(buildfilesPath, fileName), path.join(this.extensionBuildDir, fileName));
}
});
this.initialized = true;
}
// get mssql sqltoolsservice path
private async getBuildDirPathFromMssqlTools(): Promise<string> {
const mssqlConfigDir = path.join(this.extensionDir, '..', 'mssql');
if (existsSync(path.join(mssqlConfigDir, 'config.json'))) {
const rawConfig = await fs.readFile(path.join(mssqlConfigDir, 'config.json'));
const config = JSON.parse(rawConfig.toString());
const installDir = config.installDirectory?.replace('{#version#}', config.version).replace('{#platform#}', this.getPlatform());
if (installDir) {
return path.join(mssqlConfigDir, installDir);
}
}
throw new Error(mssqlNotFound(mssqlConfigDir));
}
private getPlatform(): string {
return os.platform() === 'win32' ? 'Windows' :
os.platform() === 'darwin' ? 'OSX' :
os.platform() === 'linux' ? 'Linux' :
'';
}
public get extensionBuildDirPath(): string {
return this.extensionBuildDir;
}
public constructBuildArguments(projectPath: string, buildDirPath: string): string {
projectPath = utils.getSafePath(projectPath);
buildDirPath = utils.getSafePath(buildDirPath);
return ` build ${projectPath} /p:NetCoreBuild=true /p:NETCoreTargetsPath=${buildDirPath}`;
}
}

View File

@@ -7,8 +7,10 @@ import * as vscode from 'vscode';
import * as path from 'path';
import * as fs from 'fs';
import * as os from 'os';
import { isNullOrUndefined } from 'util';
import * as cp from 'promisify-child-process';
import * as nls from 'vscode-nls';
import { isNullOrUndefined } from 'util';
import * as utils from '../common/utils';
const localize = nls.loadMessageBundle();
export const DBProjectConfigurationKey: string = 'sqlDatabaseProjects';
@@ -18,12 +20,26 @@ export const NetCoreInstallationConfirmation: string = localize('sqlDatabaseProj
export const UpdateNetCoreLocation: string = localize('sqlDatabaseProjects.UpdateNetCoreLocation', "Update .Net Core location");
export const InstallNetCore: string = localize('sqlDatabaseProjects.InstallNetCore', "Install .Net Core SDK");
const projectsOutputChannel = localize('sqlDatabaseProjects.outputChannel', "Database Projects");
const dotnet = os.platform() === 'win32' ? 'dotnet.exe' : 'dotnet';
export interface DotNetCommandOptions {
workingDirectory?: string;
additionalEnvironmentVariables?: NodeJS.ProcessEnv;
commandTitle?: string;
argument?: string;
}
export class NetCoreTool {
public findOrInstallNetCore(): void {
private _outputChannel: vscode.OutputChannel = vscode.window.createOutputChannel(projectsOutputChannel);
public findOrInstallNetCore(): boolean {
if (!this.isNetCoreInstallationPresent) {
this.showInstallDialog();
return false;
}
return true;
}
private showInstallDialog(): void {
@@ -40,7 +56,7 @@ export class NetCoreTool {
});
}
public get isNetCoreInstallationPresent(): Boolean {
private get isNetCoreInstallationPresent(): Boolean {
return (!isNullOrUndefined(this.netcoreInstallLocation) && fs.existsSync(this.netcoreInstallLocation));
}
@@ -75,4 +91,71 @@ export class NetCoreTool {
}
return undefined;
}
public async runDotnetCommand(options: DotNetCommandOptions): Promise<string> {
if (options && options.commandTitle !== undefined && options.commandTitle !== null) {
this._outputChannel.appendLine(`\t[ ${options.commandTitle} ]`);
}
if (!this.findOrInstallNetCore()) {
throw new Error(NetCoreInstallationConfirmation);
}
const dotnetPath = utils.getSafePath(path.join(this.netcoreInstallLocation, dotnet));
const command = dotnetPath + ' ' + options.argument;
try {
return await this.runStreamedCommand(command, this._outputChannel, options);
} catch (error) {
this._outputChannel.append(localize('sqlDatabaseProject.RunCommand.ErroredOut', "\t>>> {0} … errored out: {1}", command, utils.getErrorMessage(error))); //errors are localized in our code where emitted, other errors are pass through from external components that are not easily localized
throw error;
}
}
// spawns the dotnet command with aruments and redirects the error and output to ADS output channel
public async runStreamedCommand(command: string, outputChannel: vscode.OutputChannel, options?: DotNetCommandOptions): Promise<string> {
const stdoutData: string[] = [];
outputChannel.appendLine(` > ${command}`);
const spawnOptions = {
cwd: options && options.workingDirectory,
env: Object.assign({}, process.env, options && options.additionalEnvironmentVariables),
encoding: 'utf8',
maxBuffer: 10 * 1024 * 1024, // 10 Mb of output can be captured.
shell: true,
detached: false,
windowsHide: true
};
const child = cp.spawn(command, [], spawnOptions);
outputChannel.show();
// Add listeners to print stdout and stderr and exit code
child.on('exit', (code: number | null, signal: string | null) => {
if (code !== null) {
outputChannel.appendLine(localize('sqlDatabaseProjects.RunStreamedCommand.ExitedWithCode', " >>> {0} … exited with code: {1}", command, code));
} else {
outputChannel.appendLine(localize('sqlDatabaseProjects.RunStreamedCommand.ExitedWithSignal', " >>> {0} … exited with signal: {1}", command, signal));
}
});
child.stdout!.on('data', (data: string | Buffer) => {
stdoutData.push(data.toString());
this.outputDataChunk(data, outputChannel, localize('sqlDatabaseProjects.RunCommand.stdout', " stdout: "));
});
child.stderr!.on('data', (data: string | Buffer) => {
this.outputDataChunk(data, outputChannel, localize('sqlDatabaseProjects.RunCommand.stderr', " stderr: "));
});
await child;
return stdoutData.join('');
}
private outputDataChunk(data: string | Buffer, outputChannel: vscode.OutputChannel, header: string): void {
data.toString().split(/\r?\n/)
.forEach(line => {
outputChannel.appendLine(header + line);
});
}
}

View File

@@ -160,6 +160,13 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8"
integrity sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==
"@babel/runtime@^7.1.5":
version "7.9.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.6.tgz#a9102eb5cadedf3f31d08a9ecf294af7827ea29f"
integrity sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6":
version "7.8.6"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b"
@@ -680,6 +687,18 @@ postinstall-build@^5.0.1:
resolved "https://registry.yarnpkg.com/postinstall-build/-/postinstall-build-5.0.3.tgz#238692f712a481d8f5bc8960e94786036241efc7"
integrity sha512-vPvPe8TKgp4FLgY3+DfxCE5PIfoXBK2lyLfNCxsRbDsV6vS4oU5RG/IWxrblMn6heagbnMED3MemUQllQ2bQUg==
promisify-child-process@^3.1.1:
version "3.1.4"
resolved "https://registry.yarnpkg.com/promisify-child-process/-/promisify-child-process-3.1.4.tgz#3321827f283c0be30de1354bec1c6c627f02d53c"
integrity sha512-tLifJs99E4oOXUz/dKQjRgdchfiepmYQzBVrcVX9BtUWi9aGJeGSf2KgXOWBW1JFsSYgLkl1Z9HRm8i0sf4cTg==
dependencies:
"@babel/runtime" "^7.1.5"
regenerator-runtime@^0.13.4:
version "0.13.5"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697"
integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==
resolve@^1.3.2:
version "1.13.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.13.1.tgz#be0aa4c06acd53083505abb35f4d66932ab35d16"