diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 00000000..603d42b4 --- /dev/null +++ b/BUILD.md @@ -0,0 +1,75 @@ +# Usage + +Run `build.(ps1|sh)` with the desired set of arguments (see below for options). +The build script itself is `build.cake`, written in C# using the Cake build automation system. +All build related activites should be encapsulated in this file for cross-platform access. + +# Arguments + +## Primary + + `-target=TargetName`: The name of the build task/target to execute (see below for listing and details). + Defaults to `Default`. + + `-configuration=(Release|Debug)`: The configuration to build. + Defaults to `Release`. + +## Extra + + `-test-configuration=(Release|Debug)`: The configuration to use for the unit tests. + Defaults to `Debug`. + + `-install-path=Path`: Path used for the **Install** target. + Defaults to `(%USERPROFILE%|$HOME)/.sqltoolsservice/local` + + `-archive`: Enable the generation of publishable archives after a build. + +# Targets + +**Default**: Alias for Local. + +**Local**: Full build including testing for the machine-local runtime. + +**All**: Same as local, but targeting all runtimes selected by `PopulateRuntimes` in `build.cake`. + Currently configured to also build for a 32-bit Windows runtime on Windows machines. + No additional runtimes are currently selected on non-Windows machines. + +**Quick**: Local build which skips all testing. + +**Install**: Same as quick, but installs the generated binaries into `install-path`. + +**SetPackageVersions**: Updates the dependency versions found within `project.json` files using information from `depversion.json`. + Used for maintainence within the project, not needed for end-users. More information below. + +# Configuration files + +## build.json + +A number of build-related options, including folder names for different entities. Interesting options: + +**DotNetInstallScriptURL**: The URL where the .NET SDK install script is located. + Can be used to pin to a specific script version, if a breaking change occurs. + +**"DotNetChannel"**: The .NET SDK channel used for retreiving the tools. + +**"DotNetVersion"**: The .NET SDK version used for the build. Can be used to pin to a specific version. + Using the string `Latest` will retrieve the latest version. + +## depversion.json + +A listing of all dependencies (and their desired versions) used by `project.json` files throughout the project. +Allows for quick and automatic updates to the dependency version numbers using the **SetPackageVersions** target. + +# Artifacts generated + +* Binaries of Microsoft.SqlTools.ServiceLayer and its libraries built for the local machine in `artifacts/publish/Microsoft.SqlTools.ServiceLayer/default/{framework}/` +* Scripts to run Microsoft.SqlTools.ServiceLayer at `scripts/SQLTOOLSSERVICE(.Core)(.cmd)` + * These scripts are updated for every build and every install. + * The scripts point to the installed binary after and install, otherwise just the build folder (reset if a new build occurs without an install). +* Binaries of Microsoft.SqlTools.ServiceLayer and its libraries cross-compiled for other runtimes (if selected in **PopulateRuntimes**) `artifacts/publish/Microsoft.SqlTools.ServiceLayer/{runtime}/{framework}/` +* Test logs in `artifacts/logs` +* Archived binaries in `artifacts/package` (only if `-archive` used on command line) + +# Requirements + +The build system requires Mono to be installed on non-Windows machines as Cake is not built using .NET Core (yet). diff --git a/build.cake b/build.cake new file mode 100644 index 00000000..61047b05 --- /dev/null +++ b/build.cake @@ -0,0 +1,507 @@ +#addin "Newtonsoft.Json" + +#load "scripts/runhelpers.cake" +#load "scripts/archiving.cake" +#load "scripts/artifacts.cake" + +using System.ComponentModel; +using System.Net; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +// Basic arguments +var target = Argument("target", "Default"); +var configuration = Argument("configuration", "Release"); +// Optional arguments +var testConfiguration = Argument("test-configuration", "Debug"); +var installFolder = Argument("install-path", System.IO.Path.Combine(Environment.GetEnvironmentVariable(IsRunningOnWindows() ? "USERPROFILE" : "HOME"), + ".sqltoolsservice", "local")); +var requireArchive = HasArgument("archive"); + +// Working directory +var workingDirectory = System.IO.Directory.GetCurrentDirectory(); + +// System specific shell configuration +var shell = IsRunningOnWindows() ? "powershell" : "bash"; +var shellArgument = IsRunningOnWindows() ? "-NoProfile /Command" : "-C"; +var shellExtension = IsRunningOnWindows() ? "ps1" : "sh"; + +/// +/// Class representing build.json +/// +public class BuildPlan +{ + public IDictionary TestProjects { get; set; } + public string BuildToolsFolder { get; set; } + public string ArtifactsFolder { get; set; } + public bool UseSystemDotNetPath { get; set; } + public string DotNetFolder { get; set; } + public string DotNetInstallScriptURL { get; set; } + public string DotNetChannel { get; set; } + public string DotNetVersion { get; set; } + public string[] Frameworks { get; set; } + public string[] Rids { get; set; } + public string MainProject { get; set; } +} + +var buildPlan = JsonConvert.DeserializeObject( + System.IO.File.ReadAllText(System.IO.Path.Combine(workingDirectory, "build.json"))); + +// Folders and tools +var dotnetFolder = System.IO.Path.Combine(workingDirectory, buildPlan.DotNetFolder); +var dotnetcli = buildPlan.UseSystemDotNetPath ? "dotnet" : System.IO.Path.Combine(System.IO.Path.GetFullPath(dotnetFolder), "dotnet"); +var toolsFolder = System.IO.Path.Combine(workingDirectory, buildPlan.BuildToolsFolder); + +var sourceFolder = System.IO.Path.Combine(workingDirectory, "src"); +var testFolder = System.IO.Path.Combine(workingDirectory, "test"); + +var artifactFolder = System.IO.Path.Combine(workingDirectory, buildPlan.ArtifactsFolder); +var publishFolder = System.IO.Path.Combine(artifactFolder, "publish"); +var logFolder = System.IO.Path.Combine(artifactFolder, "logs"); +var packageFolder = System.IO.Path.Combine(artifactFolder, "package"); +var scriptFolder = System.IO.Path.Combine(artifactFolder, "scripts"); + +/// +/// Clean artifacts. +/// +Task("Cleanup") + .Does(() => +{ + if (System.IO.Directory.Exists(artifactFolder)) + { + System.IO.Directory.Delete(artifactFolder, true); + } + System.IO.Directory.CreateDirectory(artifactFolder); + System.IO.Directory.CreateDirectory(logFolder); + System.IO.Directory.CreateDirectory(packageFolder); + System.IO.Directory.CreateDirectory(scriptFolder); +}); + +/// +/// Pre-build setup tasks. +/// +Task("Setup") + .IsDependentOn("BuildEnvironment") + .IsDependentOn("PopulateRuntimes") + .Does(() => +{ +}); + +/// +/// Populate the RIDs for the specific environment. +/// Use default RID (+ win7-x86 on Windows) for now. +/// +Task("PopulateRuntimes") + .IsDependentOn("BuildEnvironment") + .Does(() => +{ + buildPlan.Rids = new string[] + { + "default", // To allow testing the published artifact + "win7-x64", + "win7-x86", + "ubuntu.14.04-x64", + "ubuntu.16.04-x64", + "centos.7-x64", + "rhel.7.2-x64", + "debian.8-x64", + "fedora.23-x64", + "opensuse.13.2-x64", + "osx.10.11-x64" + }; +}); + +/// +/// Install/update build environment. +/// +Task("BuildEnvironment") + .Does(() => +{ + var installScript = $"dotnet-install.{shellExtension}"; + System.IO.Directory.CreateDirectory(dotnetFolder); + var scriptPath = System.IO.Path.Combine(dotnetFolder, installScript); + using (WebClient client = new WebClient()) + { + client.DownloadFile($"{buildPlan.DotNetInstallScriptURL}/{installScript}", scriptPath); + } + if (!IsRunningOnWindows()) + { + Run("chmod", $"+x '{scriptPath}'"); + } + var installArgs = $"-Channel {buildPlan.DotNetChannel}"; + if (!String.IsNullOrEmpty(buildPlan.DotNetVersion)) + { + installArgs = $"{installArgs} -Version {buildPlan.DotNetVersion}"; + } + if (!buildPlan.UseSystemDotNetPath) + { + installArgs = $"{installArgs} -InstallDir {dotnetFolder}"; + } + Run(shell, $"{shellArgument} {scriptPath} {installArgs}"); + try + { + Run(dotnetcli, "--info"); + } + catch (Win32Exception) + { + throw new Exception(".NET CLI binary cannot be found."); + } + + System.IO.Directory.CreateDirectory(toolsFolder); + + var nugetPath = Environment.GetEnvironmentVariable("NUGET_EXE"); + var arguments = $"install xunit.runner.console -ExcludeVersion -NoCache -Prerelease -OutputDirectory \"{toolsFolder}\""; + if (IsRunningOnWindows()) + { + Run(nugetPath, arguments); + } + else + { + Run("mono", $"\"{nugetPath}\" {arguments}"); + } +}); + +/// +/// Restore required NuGet packages. +/// +Task("Restore") + .IsDependentOn("Setup") + .Does(() => +{ + RunRestore(dotnetcli, "restore", sourceFolder) + .ExceptionOnError("Failed to restore projects under source code folder."); + RunRestore(dotnetcli, "restore --infer-runtimes", testFolder) + .ExceptionOnError("Failed to restore projects under test code folder."); +}); + +/// +/// Build Test projects. +/// +Task("BuildTest") + .IsDependentOn("Setup") + .IsDependentOn("Restore") + .Does(() => +{ + foreach (var pair in buildPlan.TestProjects) + { + foreach (var framework in pair.Value) + { + var project = pair.Key; + var projectFolder = System.IO.Path.Combine(testFolder, project); + var runLog = new List(); + Run(dotnetcli, $"build --framework {framework} --configuration {testConfiguration} \"{projectFolder}\"", + new RunOptions + { + StandardOutputListing = runLog + }) + .ExceptionOnError($"Building test {project} failed for {framework}."); + System.IO.File.WriteAllLines(System.IO.Path.Combine(logFolder, $"{project}-{framework}-build.log"), runLog.ToArray()); + } + } +}); + +/// +/// Run all tests for .NET Desktop and .NET Core +/// +Task("TestAll") + .IsDependentOn("Test") + .IsDependentOn("TestCore") + .Does(() =>{}); + +/// +/// Run all tests for Travis CI .NET Desktop and .NET Core +/// +Task("TravisTestAll") + .IsDependentOn("Cleanup") + .IsDependentOn("TestAll") + .Does(() =>{}); + +/// +/// Run tests for .NET Core (using .NET CLI). +/// +Task("TestCore") + .IsDependentOn("Setup") + .IsDependentOn("Restore") + .Does(() => +{ + var testProjects = buildPlan.TestProjects + .Where(pair => pair.Value.Any(framework => framework.Contains("netcoreapp"))) + .Select(pair => pair.Key) + .ToList(); + + foreach (var testProject in testProjects) + { + var logFile = System.IO.Path.Combine(logFolder, $"{testProject}-core-result.xml"); + var testWorkingDir = System.IO.Path.Combine(testFolder, testProject); + Run(dotnetcli, $"test -f netcoreapp1.0 -xml \"{logFile}\" -notrait category=failing", testWorkingDir) + .ExceptionOnError($"Test {testProject} failed for .NET Core."); + } +}); + +/// +/// Run tests for other frameworks (using XUnit2). +/// +Task("Test") + .IsDependentOn("Setup") + .IsDependentOn("BuildTest") + .Does(() => +{ + foreach (var pair in buildPlan.TestProjects) + { + foreach (var framework in pair.Value) + { + // Testing against core happens in TestCore + if (framework.Contains("netcoreapp")) + { + continue; + } + + var project = pair.Key; + var frameworkFolder = System.IO.Path.Combine(testFolder, project, "bin", testConfiguration, framework); + var runtime = System.IO.Directory.GetDirectories(frameworkFolder).First(); + var instanceFolder = System.IO.Path.Combine(frameworkFolder, runtime); + + // Copy xunit executable to test folder to solve path errors + var xunitToolsFolder = System.IO.Path.Combine(toolsFolder, "xunit.runner.console", "tools"); + var xunitInstancePath = System.IO.Path.Combine(instanceFolder, "xunit.console.exe"); + System.IO.File.Copy(System.IO.Path.Combine(xunitToolsFolder, "xunit.console.exe"), xunitInstancePath, true); + System.IO.File.Copy(System.IO.Path.Combine(xunitToolsFolder, "xunit.runner.utility.desktop.dll"), System.IO.Path.Combine(instanceFolder, "xunit.runner.utility.desktop.dll"), true); + var targetPath = System.IO.Path.Combine(instanceFolder, $"{project}.dll"); + var logFile = System.IO.Path.Combine(logFolder, $"{project}-{framework}-result.xml"); + var arguments = $"\"{targetPath}\" -parallel none -xml \"{logFile}\" -notrait category=failing"; + if (IsRunningOnWindows()) + { + Run(xunitInstancePath, arguments, instanceFolder) + .ExceptionOnError($"Test {project} failed for {framework}"); + } + else + { + Run("mono", $"\"{xunitInstancePath}\" {arguments}", instanceFolder) + .ExceptionOnError($"Test {project} failed for {framework}"); + } + } + } +}); + +/// +/// Build, publish and package artifacts. +/// Targets all RIDs specified in build.json unless restricted by RestrictToLocalRuntime. +/// No dependencies on other tasks to support quick builds. +/// +Task("OnlyPublish") + .IsDependentOn("Setup") + .Does(() => +{ + var project = buildPlan.MainProject; + var projectFolder = System.IO.Path.Combine(sourceFolder, project); + foreach (var framework in buildPlan.Frameworks) + { + foreach (var runtime in buildPlan.Rids) + { + var outputFolder = System.IO.Path.Combine(publishFolder, project, runtime, framework); + var publishArguments = "publish"; + if (!runtime.Equals("default")) + { + publishArguments = $"{publishArguments} --runtime {runtime}"; + } + publishArguments = $"{publishArguments} --framework {framework} --configuration {configuration}"; + publishArguments = $"{publishArguments} --output \"{outputFolder}\" \"{projectFolder}\""; + Run(dotnetcli, publishArguments) + .ExceptionOnError($"Failed to publish {project} / {framework}"); + + if (requireArchive) + { + Package(runtime, framework, outputFolder, packageFolder, buildPlan.MainProject.ToLower()); + } + } + } + CreateRunScript(System.IO.Path.Combine(publishFolder, project, "default"), scriptFolder); +}); + +/// +/// Alias for OnlyPublish. +/// Targets all RIDs as specified in build.json. +/// +Task("AllPublish") + .IsDependentOn("Restore") + .IsDependentOn("OnlyPublish") + .Does(() => +{ +}); + +/// +/// Restrict the RIDs for the local default. +/// +Task("RestrictToLocalRuntime") + .IsDependentOn("Setup") + .Does(() => +{ + buildPlan.Rids = new string[] {"default"}; +}); + +/// +/// Alias for OnlyPublish. +/// Restricts publishing to local RID. +/// +Task("LocalPublish") + .IsDependentOn("Restore") + .IsDependentOn("RestrictToLocalRuntime") + .IsDependentOn("OnlyPublish") + .Does(() => +{ +}); + +/// +/// Test the published binaries if they start up without errors. +/// Uses builds corresponding to local RID. +/// +Task("TestPublished") + .IsDependentOn("Setup") + .Does(() => +{ + var project = buildPlan.MainProject; + var projectFolder = System.IO.Path.Combine(sourceFolder, project); + var scriptsToTest = new string[] {"SQLTOOLSSERVICE.Core"};//TODO + foreach (var script in scriptsToTest) + { + var scriptPath = System.IO.Path.Combine(scriptFolder, script); + var didNotExitWithError = Run($"{shell}", $"{shellArgument} \"{scriptPath}\" -s \"{projectFolder}\" --stdio", + new RunOptions + { + TimeOut = 10000 + }) + .DidTimeOut; + if (!didNotExitWithError) + { + throw new Exception($"Failed to run {script}"); + } + } +}); + +/// +/// Clean install path. +/// +Task("CleanupInstall") + .Does(() => +{ + if (System.IO.Directory.Exists(installFolder)) + { + System.IO.Directory.Delete(installFolder, true); + } + System.IO.Directory.CreateDirectory(installFolder); +}); + +/// +/// Quick build. +/// +Task("Quick") + .IsDependentOn("Cleanup") + .IsDependentOn("LocalPublish") + .Does(() => +{ +}); + +/// +/// Quick build + install. +/// +Task("Install") + .IsDependentOn("Cleanup") + .IsDependentOn("LocalPublish") + .IsDependentOn("CleanupInstall") + .Does(() => +{ + var project = buildPlan.MainProject; + foreach (var framework in buildPlan.Frameworks) + { + var outputFolder = System.IO.Path.GetFullPath(System.IO.Path.Combine(publishFolder, project, "default", framework)); + var targetFolder = System.IO.Path.GetFullPath(System.IO.Path.Combine(installFolder, framework)); + // Copy all the folders + foreach (var directory in System.IO.Directory.GetDirectories(outputFolder, "*", SearchOption.AllDirectories)) + System.IO.Directory.CreateDirectory(System.IO.Path.Combine(targetFolder, directory.Substring(outputFolder.Length + 1))); + //Copy all the files + foreach (string file in System.IO.Directory.GetFiles(outputFolder, "*", SearchOption.AllDirectories)) + System.IO.File.Copy(file, System.IO.Path.Combine(targetFolder, file.Substring(outputFolder.Length + 1)), true); + } + CreateRunScript(installFolder, scriptFolder); +}); + +/// +/// Full build targeting all RIDs specified in build.json. +/// +Task("All") + .IsDependentOn("Cleanup") + .IsDependentOn("Restore") + .IsDependentOn("TestAll") + .IsDependentOn("AllPublish") + //.IsDependentOn("TestPublished") + .Does(() => +{ +}); + +/// +/// Full build targeting local RID. +/// +Task("Local") + .IsDependentOn("Cleanup") + .IsDependentOn("Restore") + .IsDependentOn("TestAll") + .IsDependentOn("LocalPublish") + // .IsDependentOn("TestPublished") + .Does(() => +{ +}); + +/// +/// Build centered around producing the final artifacts for Travis +/// +/// The tests are run as a different task "TestAll" +/// +Task("Travis") + .IsDependentOn("Cleanup") + .IsDependentOn("Restore") + .IsDependentOn("AllPublish") + // .IsDependentOn("TestPublished") + .Does(() => +{ +}); + +/// +/// Update the package versions within project.json files. +/// Uses depversion.json file as input. +/// +Task("SetPackageVersions") + .Does(() => +{ + var jDepVersion = JObject.Parse(System.IO.File.ReadAllText(System.IO.Path.Combine(workingDirectory, "depversion.json"))); + var projects = System.IO.Directory.GetFiles(sourceFolder, "project.json", SearchOption.AllDirectories).ToList(); + projects.AddRange(System.IO.Directory.GetFiles(testFolder, "project.json", SearchOption.AllDirectories)); + foreach (var project in projects) + { + var jProject = JObject.Parse(System.IO.File.ReadAllText(project)); + var dependencies = jProject.SelectTokens("dependencies") + .Union(jProject.SelectTokens("frameworks.*.dependencies")) + .SelectMany(dependencyToken => dependencyToken.Children()); + foreach (JProperty dependency in dependencies) + { + if (jDepVersion[dependency.Name] != null) + { + dependency.Value = jDepVersion[dependency.Name]; + } + } + System.IO.File.WriteAllText(project, JsonConvert.SerializeObject(jProject, Formatting.Indented)); + } +}); + +/// +/// Default Task aliases to Local. +/// +Task("Default") + .IsDependentOn("Local") + .Does(() => +{ +}); + +/// +/// Default to Local. +/// +RunTarget(target); diff --git a/build.json b/build.json new file mode 100644 index 00000000..a0741723 --- /dev/null +++ b/build.json @@ -0,0 +1,18 @@ +{ + "UseSystemDotNetPath": "true", + "DotNetFolder": ".dotnet", + "DotNetInstallScriptURL": "https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0-preview2/scripts/obtain", + "DotNetChannel": "preview", + "DotNetVersion": "1.0.0-preview2-003121", + "BuildToolsFolder": ".tools", + "ArtifactsFolder": "artifacts", + "TestProjects": { + "Microsoft.SqlTools.ServiceLayer.Test": [ + "netcoreapp1.0" + ] + }, + "Frameworks": [ + "netcoreapp1.0" + ], + "MainProject": "Microsoft.SqlTools.ServiceLayer" +} diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 00000000..68dc2c1e --- /dev/null +++ b/build.ps1 @@ -0,0 +1,3 @@ +$Env:SQLTOOLSSERVICE_PACKAGE_OSNAME = "win-x64" +.\scripts\cake-bootstrap.ps1 -experimental @args +exit $LASTEXITCODE diff --git a/build.sh b/build.sh new file mode 100644 index 00000000..90332a42 --- /dev/null +++ b/build.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Handle to many files on osx +if [ "$TRAVIS_OS_NAME" == "osx" ] || [ `uname` == "Darwin" ]; then + ulimit -n 4096 +fi + +if [ "$TRAVIS_OS_NAME" == "osx" ] || [ `uname` == "Darwin" ]; then + export SQLTOOLSSERVICE_PACKAGE_OSNAME=osx-x64 +else + export SQLTOOLSSERVICE_PACKAGE_OSNAME=linux-x64 +fi +bash ./scripts/cake-bootstrap.sh "$@" diff --git a/scripts/archiving.cake b/scripts/archiving.cake new file mode 100644 index 00000000..4b012986 --- /dev/null +++ b/scripts/archiving.cake @@ -0,0 +1,104 @@ +#load "runhelpers.cake" + +using System.IO.Compression; +using System.Text.RegularExpressions; + +/// +/// Generate the build identifier based on the RID and framework identifier. +/// Special rules when running on Travis (for publishing purposes). +/// +/// The RID +/// The framework identifier +/// The designated build identifier +string GetBuildIdentifier(string runtime, string framework) +{ + var runtimeShort = ""; + // Default RID uses package name set in build script + if (runtime.Equals("default")) + { + runtimeShort = Environment.GetEnvironmentVariable("SQLTOOLSSERVICE_PACKAGE_OSNAME"); + } + else + { + // Remove version number. Note: because there are separate versions for Ubuntu 14 and 16, + // we treat Ubuntu as a special case. + if (runtime.StartsWith("ubuntu.14")) + { + runtimeShort = "ubuntu14-x64"; + } + else if (runtime.StartsWith("ubuntu.16")) + { + runtimeShort = "ubuntu16-x64"; + } + else + { + runtimeShort = Regex.Replace(runtime, "(\\d|\\.)*-", "-"); + } + } + + return $"{runtimeShort}-{framework}"; +} + +/// +/// Generate an archive out of the given published folder. +/// Use ZIP for Windows runtimes. +/// Use TAR.GZ for non-Windows runtimes. +/// Use 7z to generate TAR.GZ on Windows if available. +/// +/// The RID +/// The folder containing the files to package +/// The target archive name (without extension) +void DoArchive(string runtime, string contentFolder, string archiveName) +{ + // On all platforms use ZIP for Windows runtimes + if (runtime.Contains("win") || (runtime.Equals("default") && IsRunningOnWindows())) + { + var zipFile = $"{archiveName}.zip"; + Zip(contentFolder, zipFile); + } + // On all platforms use TAR.GZ for Unix runtimes + else + { + var tarFile = $"{archiveName}.tar.gz"; + // Use 7z to create TAR.GZ on Windows + if (IsRunningOnWindows()) + { + var tempFile = $"{archiveName}.tar"; + try + { + Run("7z", $"a \"{tempFile}\"", contentFolder) + .ExceptionOnError($"Tar-ing failed for {contentFolder} {archiveName}"); + Run("7z", $"a \"{tarFile}\" \"{tempFile}\"", contentFolder) + .ExceptionOnError($"Compression failed for {contentFolder} {archiveName}"); + System.IO.File.Delete(tempFile); + } + catch(Win32Exception) + { + Information("Warning: 7z not available on PATH to pack tar.gz results"); + } + } + // Use tar to create TAR.GZ on Unix + else + { + Run("tar", $"czf \"{tarFile}\" .", contentFolder) + .ExceptionOnError($"Compression failed for {contentFolder} {archiveName}"); + } + } +} + +/// +/// Package a given output folder using a build identifier generated from the RID and framework identifier. +/// +/// The RID +/// The framework identifier +/// The folder containing the files to package +/// The destination folder for the archive +/// The project name +void Package(string runtime, string framework, string contentFolder, string packageFolder, string projectName) +{ + var buildIdentifier = GetBuildIdentifier(runtime, framework); + if (buildIdentifier != null) + { + DoArchive(runtime, contentFolder, $"{packageFolder}/{projectName}-{buildIdentifier}"); + } +} \ No newline at end of file diff --git a/scripts/artifacts.cake b/scripts/artifacts.cake new file mode 100644 index 00000000..f448fe3f --- /dev/null +++ b/scripts/artifacts.cake @@ -0,0 +1,43 @@ +#load "runhelpers.cake" + +/// +/// Generate the scripts which target the SQLTOOLSSERVICE binaries. +/// +/// The root folder where the publised (or installed) binaries are located +void CreateRunScript(string outputRoot, string scriptFolder) +{ + if (IsRunningOnWindows()) + { + var coreScript = System.IO.Path.Combine(scriptFolder, "SQLTOOLSSERVICE.Core.cmd"); + var sqlToolsServicePath = System.IO.Path.Combine(System.IO.Path.GetFullPath(outputRoot), "{0}", "SQLTOOLSSERVICE"); + var content = new string[] { + "SETLOCAL", + "", + $"\"{sqlToolsServicePath}\" %*" + }; + if (System.IO.File.Exists(coreScript)) + { + System.IO.File.Delete(coreScript); + } + content[2] = String.Format(content[2], "netcoreapp1.0"); + System.IO.File.WriteAllLines(coreScript, content); + } + else + { + var coreScript = System.IO.Path.Combine(scriptFolder, "SQLTOOLSSERVICE.Core"); + var sqlToolsServicePath = System.IO.Path.Combine(System.IO.Path.GetFullPath(outputRoot), "{1}", "SQLTOOLSSERVICE"); + var content = new string[] { + "#!/bin/bash", + "", + $"{{0}} \"{sqlToolsServicePath}{{2}}\" \"$@\"" + }; + + if (System.IO.File.Exists(coreScript)) + { + System.IO.File.Delete(coreScript); + } + content[2] = String.Format(content[2], "", "netcoreapp1.0", ""); + System.IO.File.WriteAllLines(coreScript, content); + Run("chmod", $"+x \"{coreScript}\""); + } +} \ No newline at end of file diff --git a/scripts/cake-bootstrap.ps1 b/scripts/cake-bootstrap.ps1 new file mode 100644 index 00000000..a87c1478 --- /dev/null +++ b/scripts/cake-bootstrap.ps1 @@ -0,0 +1,110 @@ +<# + +.SYNOPSIS +This is a Powershell script to bootstrap a Cake build. + +.DESCRIPTION +This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) +and execute your Cake build script with the parameters you provide. + +.PARAMETER Script +The build script to execute. +.PARAMETER Target +The build script target to run. +.PARAMETER Configuration +The build configuration to use. +.PARAMETER Verbosity +Specifies the amount of information to be displayed. +Tells Cake to use the latest Roslyn release. +.PARAMETER WhatIf +Performs a dry run of the build script. +No tasks will be executed. +.PARAMETER Mono +Tells Cake to use the Mono scripting engine. + +.LINK +http://cakebuild.net + +#> + +[CmdletBinding()] +Param( + [string]$Script = "build.cake", + [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] + [string]$Verbosity = "Verbose", + [Alias("DryRun","Noop")] + [switch]$WhatIf, + [switch]$Mono, + [switch]$SkipToolPackageRestore, + [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] + [string[]]$ScriptArgs +) + +Write-Host "Preparing to run build script..." + +$PS_SCRIPT_ROOT = split-path -parent $MyInvocation.MyCommand.Definition; +$TOOLS_DIR = Join-Path $PSScriptRoot "..\.tools" +$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" +$NUGET_URL = "https://dist.nuget.org/win-x86-commandline/v3.3.0/nuget.exe" +$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" +$PACKAGES_CONFIG = Join-Path $PS_SCRIPT_ROOT "packages.config" + +# Should we use mono? +$UseMono = ""; +if($Mono.IsPresent) { + Write-Verbose -Message "Using the Mono based scripting engine." + $UseMono = "-mono" +} + +# Is this a dry run? +$UseDryRun = ""; +if($WhatIf.IsPresent) { + $UseDryRun = "-dryrun" +} + +# Make sure tools folder exists +if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { + Write-Verbose -Message "Creating tools directory..." + New-Item -Path $TOOLS_DIR -Type directory | out-null +} + +# Try download NuGet.exe if not exists +if (!(Test-Path $NUGET_EXE)) { + Write-Verbose -Message "Downloading NuGet.exe..." + try { + (New-Object System.Net.WebClient).DownloadFile($NUGET_URL, $NUGET_EXE) + } catch { + Throw "Could not download NuGet.exe." + } +} + +# Save nuget.exe path to environment to be available to child processed +$ENV:NUGET_EXE = $NUGET_EXE + +# Restore tools from NuGet? +if(-Not $SkipToolPackageRestore.IsPresent) +{ + # Restore packages from NuGet. + Push-Location + Set-Location $TOOLS_DIR + + Write-Verbose -Message "Restoring tools from NuGet..." + $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install $PACKAGES_CONFIG -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" + Write-Verbose -Message ($NuGetOutput | out-string) + + Pop-Location + if ($LASTEXITCODE -ne 0) + { + exit $LASTEXITCODE + } +} + +# Make sure that Cake has been installed. +if (!(Test-Path $CAKE_EXE)) { + Throw "Could not find Cake.exe at $CAKE_EXE" +} + +# Start Cake +Write-Host "Running build script..." +Invoke-Expression "& `"$CAKE_EXE`" `"$Script`" -verbosity=`"$Verbosity`" $UseMono $UseDryRun $ScriptArgs" +exit $LASTEXITCODE diff --git a/scripts/cake-bootstrap.sh b/scripts/cake-bootstrap.sh new file mode 100644 index 00000000..abc3ed43 --- /dev/null +++ b/scripts/cake-bootstrap.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +############################################################### +# This is the Cake bootstrapper script that is responsible for +# downloading Cake and all specified tools from NuGet. +############################################################### + +# Define directories. +SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +TOOLS_DIR=$SCRIPT_DIR/../.tools +export NUGET_EXE=$TOOLS_DIR/nuget.exe +CAKE_EXE=$TOOLS_DIR/Cake/Cake.exe +PACKAGES_CONFIG=$SCRIPT_DIR/packages.config + +# Define default arguments. +SCRIPT="build.cake" +VERBOSITY="verbose" +DRYRUN= +SHOW_VERSION=false +SCRIPT_ARGUMENTS=() + +# Parse arguments. +for i in "$@"; do + case $1 in + -s|--script) SCRIPT="$2"; shift ;; + -v|--verbosity) VERBOSITY="$2"; shift ;; + -d|--dryrun) DRYRUN="-dryrun" ;; + --version) SHOW_VERSION=true ;; + --) shift; SCRIPT_ARGUMENTS+=("$@"); break ;; + *) SCRIPT_ARGUMENTS+=("$1") ;; + esac + shift +done + +# Make sure the tools folder exist. +if [ ! -d "$TOOLS_DIR" ]; then + mkdir "$TOOLS_DIR" +fi + +# Download NuGet if it does not exist. +if [ ! -f "$NUGET_EXE" ]; then + echo "Downloading NuGet..." + curl -Lsfo "$NUGET_EXE" https://dist.nuget.org/win-x86-commandline/v3.3.0/nuget.exe + if [ $? -ne 0 ]; then + echo "An error occured while downloading nuget.exe." + exit 1 + fi +fi + +# Restore tools from NuGet. +pushd "$TOOLS_DIR" >/dev/null +mono "$NUGET_EXE" install "$PACKAGES_CONFIG" -ExcludeVersion -OutputDirectory "$TOOLS_DIR" +if [ $? -ne 0 ]; then + echo "Could not restore NuGet packages." + exit 1 +fi +popd >/dev/null + +# Make sure that Cake has been installed. +if [ ! -f "$CAKE_EXE" ]; then + echo "Could not find Cake.exe at '$CAKE_EXE'." + exit 1 +fi + +# Start Cake +if $SHOW_VERSION; then + exec mono "$CAKE_EXE" -version +else + exec mono "$CAKE_EXE" $SCRIPT -verbosity=$VERBOSITY $DRYRUN "${SCRIPT_ARGUMENTS[@]}" +fi diff --git a/scripts/packages.config b/scripts/packages.config new file mode 100644 index 00000000..c4feb50f --- /dev/null +++ b/scripts/packages.config @@ -0,0 +1,5 @@ + + + + + diff --git a/scripts/runhelpers.cake b/scripts/runhelpers.cake new file mode 100644 index 00000000..03499601 --- /dev/null +++ b/scripts/runhelpers.cake @@ -0,0 +1,204 @@ +using System.Collections.Generic; +using System.Diagnostics; + +/// +/// Class encompassing the optional settings for running processes. +/// +public class RunOptions +{ + /// + /// The working directory of the process. + /// + public string WorkingDirectory { get; set; } + /// + /// Container logging the StandardOutput content. + /// + public IList StandardOutputListing { get; set; } + /// + /// Desired maximum time-out for the process + /// + public int TimeOut { get; set; } +} + +/// +/// Wrapper for the exit code and state. +/// Used to query the result of an execution with method calls. +/// +public struct ExitStatus +{ + private int _code; + private bool _timeOut; + /// + /// Default constructor when the execution finished. + /// + /// The exit code + public ExitStatus(int code) + { + this._code = code; + this._timeOut = false; + } + /// + /// Default constructor when the execution potentially timed out. + /// + /// The exit code + /// True if the execution timed out + public ExitStatus(int code, bool timeOut) + { + this._code = code; + this._timeOut = timeOut; + } + /// + /// Flag signalling that the execution timed out. + /// + public bool DidTimeOut { get { return _timeOut; } } + /// + /// Implicit conversion from ExitStatus to the exit code. + /// + /// The exit status + /// The exit code + public static implicit operator int(ExitStatus exitStatus) + { + return exitStatus._code; + } + /// + /// Trigger Exception for non-zero exit code. + /// + /// The message to use in the Exception + /// The exit status for further queries + public ExitStatus ExceptionOnError(string errorMessage) + { + if (this._code != 0) + { + throw new Exception(errorMessage); + } + return this; + } +} + +/// +/// Run the given executable with the given arguments. +/// +/// Executable to run +/// Arguments +/// The exit status for further queries +ExitStatus Run(string exec, string args) +{ + return Run(exec, args, new RunOptions()); +} + +/// +/// Run the given executable with the given arguments. +/// +/// Executable to run +/// Arguments +/// Working directory +/// The exit status for further queries +ExitStatus Run(string exec, string args, string workingDirectory) +{ + return Run(exec, args, + new RunOptions() + { + WorkingDirectory = workingDirectory + }); +} + +/// +/// Run the given executable with the given arguments. +/// +/// Executable to run +/// Arguments +/// Optional settings +/// The exit status for further queries +ExitStatus Run(string exec, string args, RunOptions runOptions) +{ + var workingDirectory = runOptions.WorkingDirectory ?? System.IO.Directory.GetCurrentDirectory(); + var process = System.Diagnostics.Process.Start( + new ProcessStartInfo(exec, args) + { + WorkingDirectory = workingDirectory, + UseShellExecute = false, + RedirectStandardOutput = runOptions.StandardOutputListing != null + }); + if (runOptions.StandardOutputListing != null) + { + process.OutputDataReceived += (s, e) => + { + if (e.Data != null) + { + runOptions.StandardOutputListing.Add(e.Data); + } + }; + process.BeginOutputReadLine(); + } + if (runOptions.TimeOut == 0) + { + process.WaitForExit(); + return new ExitStatus(process.ExitCode); + } + else + { + bool finished = process.WaitForExit(runOptions.TimeOut); + if (finished) + { + return new ExitStatus(process.ExitCode); + } + else + { + KillProcessTree(process); + return new ExitStatus(0, true); + } + } +} + +/// +/// Run restore with the given arguments +/// +/// Executable to run +/// Arguments +/// Optional settings +/// The exit status for further queries +ExitStatus RunRestore(string exec, string args, string workingDirectory) +{ + Information("Restoring packages...."); + var p = StartAndReturnProcess(exec, + new ProcessSettings + { + Arguments = args, + RedirectStandardOutput = true, + WorkingDirectory = workingDirectory + }); + p.WaitForExit(); + var exitCode = p.GetExitCode(); + + if (exitCode == 0) + { + Information("Package restore successful!"); + } + else + { + Error(string.Join("\n", p.GetStandardOutput())); + } + return new ExitStatus(exitCode); +} + +/// +/// Kill the given process and all its child processes. +/// +/// Root process +public void KillProcessTree(Process process) +{ + // Child processes are not killed on Windows by default + // Use TASKKILL to kill the process hierarchy rooted in the process + if (IsRunningOnWindows()) + { + StartProcess($"TASKKILL", + new ProcessSettings + { + Arguments = $"/PID {process.Id} /T /F", + }); + } + else + { + process.Kill(); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/project.json b/src/Microsoft.SqlTools.ServiceLayer/project.json index a0a73439..02e977aa 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/project.json +++ b/src/Microsoft.SqlTools.ServiceLayer/project.json @@ -13,18 +13,29 @@ "Microsoft.SqlServer.Smo": "140.1.5", "System.Security.SecureString": "4.0.0", "System.Collections.Specialized": "4.0.1", - "System.ComponentModel.TypeConverter": "4.1.0", - "System.Diagnostics.TraceSource": "4.0.0" + "System.ComponentModel.TypeConverter": "4.1.0", + "System.Diagnostics.TraceSource": "4.0.0", + "NETStandard.Library": "1.6.0", + "Microsoft.NETCore.Runtime.CoreCLR": "1.0.2", + "Microsoft.NETCore.DotNetHostPolicy": "1.0.1", + "System.Diagnostics.Process": "4.1.0", + "System.Threading.Thread": "4.0.0" }, "frameworks": { "netcoreapp1.0": { - "dependencies": { - "Microsoft.NETCore.App": { - "type": "platform", - "version": "1.0.0" - } - }, - "imports": "dnxcore50" + "imports": "dnxcore50" } + }, + "runtimes": { + "win7-x64": {}, + "win7-x86": {}, + "osx.10.11-x64": {}, + "ubuntu.14.04-x64": {}, + "ubuntu.16.04-x64": {}, + "centos.7-x64": {}, + "rhel.7.2-x64": {}, + "debian.8-x64": {}, + "fedora.23-x64": {}, + "opensuse.13.2-x64": {} } }