Add docker support
This commit is contained in:
Родитель
49987ee31b
Коммит
8878da920f
|
@ -1,5 +1,7 @@
|
||||||
language: csharp
|
language: csharp
|
||||||
sudo: false
|
sudo: false
|
||||||
|
services:
|
||||||
|
- docker
|
||||||
dist: trusty
|
dist: trusty
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
|
|
|
@ -5,6 +5,7 @@ __korebuild_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
source "$__korebuild_dir/scripts/common.sh"
|
source "$__korebuild_dir/scripts/common.sh"
|
||||||
|
|
||||||
# functions
|
# functions
|
||||||
|
default_tools_source='https://aspnetcore.blob.core.windows.net/buildtools'
|
||||||
|
|
||||||
set_korebuildsettings() {
|
set_korebuildsettings() {
|
||||||
tools_source=$1
|
tools_source=$1
|
||||||
|
@ -13,7 +14,7 @@ set_korebuildsettings() {
|
||||||
local config_file="${4:-}" # optional. Not used yet.
|
local config_file="${4:-}" # optional. Not used yet.
|
||||||
|
|
||||||
[ -z "${dot_net_home:-}" ] && dot_net_home="$HOME/.dotnet"
|
[ -z "${dot_net_home:-}" ] && dot_net_home="$HOME/.dotnet"
|
||||||
[ -z "${tools_source:-}" ] && tools_source='https://aspnetcore.blob.core.windows.net/buildtools'
|
[ -z "${tools_source:-}" ] && tools_source="$default_tools_source"
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
@ -74,7 +75,8 @@ install_tools() {
|
||||||
|
|
||||||
# Call "sync" between "chmod" and execution to prevent "text file busy" error in Docker (aufs)
|
# Call "sync" between "chmod" and execution to prevent "text file busy" error in Docker (aufs)
|
||||||
chmod +x "$__korebuild_dir/scripts/get-netfx.sh"; sync
|
chmod +x "$__korebuild_dir/scripts/get-netfx.sh"; sync
|
||||||
"$__korebuild_dir/scripts/get-netfx.sh" $verbose_flag $netfx_version "$tools_source" "$ReferenceAssemblyRoot" \
|
# we don't include netfx in the BuildTools artifacts currently, it ends up on the blob store through other means, so we'll only look for it in the default_tools_source
|
||||||
|
"$__korebuild_dir/scripts/get-netfx.sh" $verbose_flag $netfx_version "$default_tools_source" "$ReferenceAssemblyRoot" \
|
||||||
|| return 1
|
|| return 1
|
||||||
|
|
||||||
# Call "sync" between "chmod" and execution to prevent "text file busy" error in Docker (aufs)
|
# Call "sync" between "chmod" and execution to prevent "text file busy" error in Docker (aufs)
|
||||||
|
|
|
@ -327,7 +327,7 @@ function Invoke-KoreBuildCommand(
|
||||||
--tools-source $global:KoreBuildSettings.ToolsSource `
|
--tools-source $global:KoreBuildSettings.ToolsSource `
|
||||||
--dotnet-home $global:KoreBuildSettings.DotNetHome `
|
--dotnet-home $global:KoreBuildSettings.DotNetHome `
|
||||||
--repo-path $global:KoreBuildSettings.RepoPath `
|
--repo-path $global:KoreBuildSettings.RepoPath `
|
||||||
$Arguments
|
@Arguments
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,13 +29,13 @@ __usage() {
|
||||||
echo " <Arguments>... Arguments passed to the command. Variable number of arguments allowed."
|
echo " <Arguments>... Arguments passed to the command. Variable number of arguments allowed."
|
||||||
echo ""
|
echo ""
|
||||||
echo "Options:"
|
echo "Options:"
|
||||||
echo " --verbose Show verbose output."
|
echo " --verbose Show verbose output."
|
||||||
echo " -c|--channel <CHANNEL> The channel of KoreBuild to download. Overrides the value from the config file.."
|
echo " -c|--channel <CHANNEL> The channel of KoreBuild to download. Overrides the value from the config file.."
|
||||||
echo " --config-file <FILE> TThe path to the configuration file that stores values. Defaults to korebuild.json."
|
echo " --config-file <FILE> The path to the configuration file that stores values. Defaults to korebuild.json."
|
||||||
echo " -d|--dotnet-home <DIR> The directory where .NET Core tools will be stored. Defaults to '\$DOTNET_HOME' or '\$HOME/.dotnet."
|
echo " -d|--dotnet-home <DIR> The directory where .NET Core tools will be stored. Defaults to '\$DOTNET_HOME' or '\$HOME/.dotnet."
|
||||||
echo " --path <PATH> The directory to build. Defaults to the directory containing the script."
|
echo " --path <PATH> The directory to build. Defaults to the directory containing the script."
|
||||||
echo " -s|--tools-source <URL> The base url where build tools can be downloaded. Overrides the value from the config file."
|
echo " -s|--tools-source|-ToolsSource <URL> The base url where build tools can be downloaded. Overrides the value from the config file."
|
||||||
echo " -u|--update Update to the latest KoreBuild even if the lock file is present."
|
echo " -u|--update Update to the latest KoreBuild even if the lock file is present."
|
||||||
echo ""
|
echo ""
|
||||||
echo "Description:"
|
echo "Description:"
|
||||||
echo " This function will create a file \$DIR/korebuild-lock.txt. This lock file can be committed to source, but does not have to be."
|
echo " This function will create a file \$DIR/korebuild-lock.txt. This lock file can be committed to source, but does not have to be."
|
||||||
|
|
|
@ -2,8 +2,11 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
@ -55,5 +58,85 @@ namespace KoreBuild.FunctionalTests
|
||||||
Assert.Same(task, build);
|
Assert.Same(task, build);
|
||||||
Assert.NotEqual(0, build.Result);
|
Assert.NotEqual(0, build.Result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[DockerExistsFact]
|
||||||
|
public async Task DockerSuccessful()
|
||||||
|
{
|
||||||
|
var app = _fixture.CreateTestApp("SimpleRepo");
|
||||||
|
var platform = "jessie";
|
||||||
|
|
||||||
|
var dockerPlatform = GetDockerPlatform();
|
||||||
|
if (dockerPlatform == OSPlatform.Windows)
|
||||||
|
{
|
||||||
|
platform = "winservercore";
|
||||||
|
}
|
||||||
|
|
||||||
|
var build = app.ExecuteRun(_output, new string[]{ "docker-build", "-Path", app.WorkingDirectory}, platform, "/p:BuildNumber=0001");
|
||||||
|
var task = await Task.WhenAny(build, Task.Delay(TimeSpan.FromMinutes(10)));
|
||||||
|
|
||||||
|
Assert.Same(task, build);
|
||||||
|
|
||||||
|
Assert.Equal(0, build.Result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OSPlatform GetDockerPlatform()
|
||||||
|
{
|
||||||
|
var startInfo = new ProcessStartInfo("docker", @"version -f ""{{ .Server.Os }}""")
|
||||||
|
{
|
||||||
|
RedirectStandardOutput = true
|
||||||
|
};
|
||||||
|
|
||||||
|
using (var process = Process.Start(startInfo))
|
||||||
|
{
|
||||||
|
var output = process.StandardOutput.ReadToEnd().Trim();
|
||||||
|
|
||||||
|
OSPlatform result;
|
||||||
|
switch(output)
|
||||||
|
{
|
||||||
|
case "windows":
|
||||||
|
result = OSPlatform.Windows;
|
||||||
|
break;
|
||||||
|
case "linux":
|
||||||
|
result = OSPlatform.Linux;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NotImplementedException($"No default for docker platform {output}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||||
|
public class DockerExistsFactAttribute : FactAttribute
|
||||||
|
{
|
||||||
|
public DockerExistsFactAttribute()
|
||||||
|
{
|
||||||
|
if(!HasDocker())
|
||||||
|
{
|
||||||
|
Skip = "Docker must be installed to run this test.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasDocker()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var startInfo = new ProcessStartInfo("docker", "--version")
|
||||||
|
{
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true
|
||||||
|
};
|
||||||
|
using (Process.Start(startInfo))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Win32Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,17 @@ namespace KoreBuild.FunctionalTests
|
||||||
|
|
||||||
public string WorkingDirectory { get; }
|
public string WorkingDirectory { get; }
|
||||||
|
|
||||||
public async Task<int> ExecuteBuild(ITestOutputHelper output, params string[] args)
|
public async Task<int> ExecuteRun(ITestOutputHelper output, string[] koreBuildArgs, params string[] commandArgs)
|
||||||
|
{
|
||||||
|
return await ExecuteScript(output, "run", koreBuildArgs, commandArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> ExecuteBuild(ITestOutputHelper output, params string[] commandArgs)
|
||||||
|
{
|
||||||
|
return await ExecuteScript(output, "build", new string[0], commandArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<int> ExecuteScript(ITestOutputHelper output, string script, string[] koreBuildArgs, params string[] commandArgs)
|
||||||
{
|
{
|
||||||
output.WriteLine("Starting in " + WorkingDirectory);
|
output.WriteLine("Starting in " + WorkingDirectory);
|
||||||
void Write(object sender, DataReceivedEventArgs e)
|
void Write(object sender, DataReceivedEventArgs e)
|
||||||
|
@ -41,22 +51,25 @@ namespace KoreBuild.FunctionalTests
|
||||||
{
|
{
|
||||||
cmd = "cmd.exe";
|
cmd = "cmd.exe";
|
||||||
arguments.Add("/C");
|
arguments.Add("/C");
|
||||||
arguments.Add(@".\build.cmd");
|
arguments.Add($@".\{script}.cmd");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
cmd = "bash";
|
cmd = "bash";
|
||||||
arguments.Add("./build.sh");
|
arguments.Add($"./{script}.sh");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
arguments.AddRange(koreBuildArgs);
|
||||||
|
|
||||||
arguments.AddRange(new[]
|
arguments.AddRange(new[]
|
||||||
{
|
{
|
||||||
"-ToolsSource", _toolsSource,
|
"-ToolsSource", _toolsSource,
|
||||||
"-Update"
|
"-Update"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
arguments.AddRange(commandArgs);
|
||||||
|
|
||||||
arguments.Add("/v:n");
|
arguments.Add("/v:n");
|
||||||
arguments.AddRange(args);
|
|
||||||
|
|
||||||
var process = new Process
|
var process = new Process
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,182 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
|
||||||
|
namespace KoreBuild.Console.Commands
|
||||||
|
{
|
||||||
|
internal class DockerBuildCommand : SubCommandBase
|
||||||
|
{
|
||||||
|
private const string DockerIgnore = ".dockerignore";
|
||||||
|
private const string DockerfileExtension = ".dockerfile";
|
||||||
|
private const string Owner = "aspnetbuild";
|
||||||
|
private const string ImageName = "korebuild";
|
||||||
|
|
||||||
|
public CommandArgument ImageVariant { get; set; }
|
||||||
|
|
||||||
|
public List<string> Arguments {get; set; }
|
||||||
|
|
||||||
|
public string Tag => $@"{Owner}/{ImageName}:{ImageVariant.Value}";
|
||||||
|
|
||||||
|
public override void Configure(CommandLineApplication application)
|
||||||
|
{
|
||||||
|
ImageVariant = application.Argument("image", "The docker image to run on.");
|
||||||
|
Arguments = application.RemainingArguments;
|
||||||
|
|
||||||
|
base.Configure(application);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsValid()
|
||||||
|
{
|
||||||
|
if(string.IsNullOrEmpty(ImageVariant?.Value))
|
||||||
|
{
|
||||||
|
Reporter.Error("Image is a required argument.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override int Execute()
|
||||||
|
{
|
||||||
|
var dockerFileName = GetDockerFileName(ImageVariant.Value);
|
||||||
|
var dockerFileSource = GetDockerFileSource(dockerFileName);
|
||||||
|
var dockerFileDestination = Path.Combine(RepoPath, GetDockerFileName(ImageVariant.Value));
|
||||||
|
|
||||||
|
File.Copy(dockerFileSource, dockerFileDestination, overwrite: true);
|
||||||
|
|
||||||
|
var dockerIgnoreSource = GetDockerFileSource(DockerIgnore);
|
||||||
|
var dockerIgnoreDestination = Path.Combine(RepoPath, DockerIgnore);
|
||||||
|
|
||||||
|
File.Copy(dockerIgnoreSource, dockerIgnoreDestination, overwrite: true);
|
||||||
|
|
||||||
|
// If our ToolSource isn't http copy it to the docker context
|
||||||
|
var dockerToolsSource = ToolsSource;
|
||||||
|
string toolsSourceDestination = null;
|
||||||
|
if (!ToolsSource.StartsWith("http"))
|
||||||
|
{
|
||||||
|
dockerToolsSource = "ToolsSource";
|
||||||
|
toolsSourceDestination = Path.Combine(RepoPath, dockerToolsSource);
|
||||||
|
DirectoryCopy(ToolsSource, toolsSourceDestination);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var buildArgs = new List<string> { "build" };
|
||||||
|
|
||||||
|
buildArgs.AddRange(new string[] { "-t", Tag, "-f", dockerFileDestination, RepoPath });
|
||||||
|
var buildResult = RunDockerCommand(buildArgs);
|
||||||
|
|
||||||
|
if (buildResult != 0)
|
||||||
|
{
|
||||||
|
return buildResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
var containerName = $"{Owner}_{DateTime.Now.ToString("yyyyMMddHHmmss")}";
|
||||||
|
|
||||||
|
var runArgs = new List<string> { "run", "--rm", "-it", "--name", containerName, Tag };
|
||||||
|
|
||||||
|
runArgs.AddRange(new[] { "-ToolsSource", dockerToolsSource });
|
||||||
|
|
||||||
|
if (Arguments?.Count > 0)
|
||||||
|
{
|
||||||
|
runArgs.AddRange(Arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
Reporter.Verbose($"Running in container '{containerName}'");
|
||||||
|
return RunDockerCommand(runArgs);
|
||||||
|
}
|
||||||
|
finally{
|
||||||
|
// Clean up the stuff we dumped there in order to get it in the docker context.
|
||||||
|
File.Delete(dockerFileDestination);
|
||||||
|
File.Delete(dockerIgnoreDestination);
|
||||||
|
if(toolsSourceDestination != null)
|
||||||
|
{
|
||||||
|
Directory.Delete(toolsSourceDestination, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetDockerFileSource(string fileName)
|
||||||
|
{
|
||||||
|
var executingDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||||
|
var source = Path.Combine(executingDir, "Commands", "DockerFiles", fileName);
|
||||||
|
|
||||||
|
if(!File.Exists(source))
|
||||||
|
{
|
||||||
|
Reporter.Error($"DockerFile '{source}' doesn't exist.");
|
||||||
|
throw new FileNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetDockerFileName(string platform)
|
||||||
|
{
|
||||||
|
return $"{platform}{DockerfileExtension}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private int RunDockerCommand(List<string> arguments)
|
||||||
|
{
|
||||||
|
var args = ArgumentEscaper.EscapeAndConcatenate(arguments.ToArray());
|
||||||
|
Reporter.Verbose($"Running 'docker {args}'");
|
||||||
|
|
||||||
|
var psi = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "docker",
|
||||||
|
Arguments = args,
|
||||||
|
RedirectStandardError = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var process = Process.Start(psi);
|
||||||
|
process.WaitForExit();
|
||||||
|
|
||||||
|
if(process.ExitCode != 0)
|
||||||
|
{
|
||||||
|
Reporter.Error(process.StandardError.ReadToEnd());
|
||||||
|
}
|
||||||
|
|
||||||
|
return process.ExitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DirectoryCopy(string sourceDirName, string destDirName)
|
||||||
|
{
|
||||||
|
// Get the subdirectories for the specified directory.
|
||||||
|
DirectoryInfo dir = new DirectoryInfo(sourceDirName);
|
||||||
|
|
||||||
|
if (!dir.Exists)
|
||||||
|
{
|
||||||
|
throw new DirectoryNotFoundException(
|
||||||
|
"Source directory does not exist or could not be found: "
|
||||||
|
+ sourceDirName);
|
||||||
|
}
|
||||||
|
|
||||||
|
DirectoryInfo[] dirs = dir.GetDirectories();
|
||||||
|
// If the destination directory doesn't exist, create it.
|
||||||
|
if (!Directory.Exists(destDirName))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(destDirName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the files in the directory and copy them to the new location.
|
||||||
|
FileInfo[] files = dir.GetFiles();
|
||||||
|
foreach (FileInfo file in files)
|
||||||
|
{
|
||||||
|
string temppath = Path.Combine(destDirName, file.Name);
|
||||||
|
file.CopyTo(temppath, overwrite: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy subdirectories and their contents to the new location.
|
||||||
|
foreach (DirectoryInfo subdir in dirs)
|
||||||
|
{
|
||||||
|
string temppath = Path.Combine(destDirName, subdir.Name);
|
||||||
|
DirectoryCopy(subdir.FullName, temppath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
korebuild-lock.txt
|
||||||
|
**/bin
|
||||||
|
**/obj
|
||||||
|
artifacts
|
|
@ -0,0 +1,16 @@
|
||||||
|
FROM microsoft/dotnet:2.0-runtime-deps-jessie
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
git \
|
||||||
|
# KoreBuild dependencies
|
||||||
|
curl \
|
||||||
|
unzip \
|
||||||
|
apt-transport-https \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ADD . .
|
||||||
|
|
||||||
|
ENV DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true
|
||||||
|
|
||||||
|
ENTRYPOINT ["./build.sh"]
|
|
@ -0,0 +1,18 @@
|
||||||
|
FROM microsoft/aspnet:4.6.2
|
||||||
|
|
||||||
|
|
||||||
|
# DevPack returns exit 0 immediately, but it's not done, so we wait.
|
||||||
|
# A more correct thing would be to block on a registry key existing or similar.
|
||||||
|
RUN \
|
||||||
|
Invoke-WebRequest https://download.microsoft.com/download/F/1/D/F1DEB8DB-D277-4EF9-9F48-3A65D4D8F965/NDP461-DevPack-KB3105179-ENU.exe -OutFile ~\\net461dev.exe ; \
|
||||||
|
~\\net461dev.exe /Passive /NoRestart ; \
|
||||||
|
Start-Sleep -s 10; \
|
||||||
|
Remove-Item ~\\net461dev.exe -Force ;
|
||||||
|
|
||||||
|
WORKDIR c:\\repo
|
||||||
|
|
||||||
|
ADD . .
|
||||||
|
|
||||||
|
ENV DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true
|
||||||
|
|
||||||
|
ENTRYPOINT ["build.cmd"]
|
|
@ -14,6 +14,7 @@ namespace KoreBuild.Console.Commands
|
||||||
|
|
||||||
application.Command("install-tools", new InstallToolsCommand().Configure, throwOnUnexpectedArg:false);
|
application.Command("install-tools", new InstallToolsCommand().Configure, throwOnUnexpectedArg:false);
|
||||||
application.Command("msbuild", new MSBuildCommand().Configure, throwOnUnexpectedArg:false);
|
application.Command("msbuild", new MSBuildCommand().Configure, throwOnUnexpectedArg:false);
|
||||||
|
application.Command("docker-build", new DockerBuildCommand().Configure, throwOnUnexpectedArg: false);
|
||||||
|
|
||||||
application.VersionOption("--version", GetVersion);
|
application.VersionOption("--version", GetVersion);
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="Commands\DockerFiles\*.dockerfile" CopyToPublishDirectory="PreserveNewest" CopyToOutputDirectory="PreserveNewest" />
|
<Content Include="Commands\DockerFiles\*.dockerfile" CopyToPublishDirectory="PreserveNewest" CopyToOutputDirectory="PreserveNewest" />
|
||||||
|
<Content Include="Commands\DockerFiles\.dockerignore" CopyToPublishDirectory="PreserveNewest" CopyToOutputDirectory="PreserveNewest" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
Загрузка…
Ссылка в новой задаче