diff --git a/contrib/DockerFileBuildHelper/DockerFile.cs b/contrib/DockerFileBuildHelper/DockerFile.cs new file mode 100644 index 0000000..b1d7d82 --- /dev/null +++ b/contrib/DockerFileBuildHelper/DockerFile.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace DockerFileBuildHelper +{ + public class DockerFile + { + public string DockerFileName { get; private set; } + public string DockerFilePath { get; private set; } + + public static DockerFile Parse(string str) + { + var file = new DockerFile(); + var lastPart = str.LastIndexOf('/'); + file.DockerFileName = str.Substring(lastPart + 1); + if (lastPart == -1) + { + file.DockerFilePath = "."; + } + else + { + file.DockerFilePath = str.Substring(0, lastPart); + } + return file; + } + } +} diff --git a/contrib/DockerFileBuildHelper/DockerFileBuildHelper.csproj b/contrib/DockerFileBuildHelper/DockerFileBuildHelper.csproj new file mode 100644 index 0000000..5a77ed4 --- /dev/null +++ b/contrib/DockerFileBuildHelper/DockerFileBuildHelper.csproj @@ -0,0 +1,10 @@ + + + + Exe + netcoreapp2.1 + + + + + diff --git a/contrib/DockerFileBuildHelper/DockerInfo.cs b/contrib/DockerFileBuildHelper/DockerInfo.cs new file mode 100644 index 0000000..ace3445 --- /dev/null +++ b/contrib/DockerFileBuildHelper/DockerInfo.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace DockerFileBuildHelper +{ + public class DockerInfo + { + public string DockerFilePath { get; set; } + public string DockerFilePathARM32v7 { get; set; } + public string DockerFilePathARM64v8 { get; set; } + public string DockerHubLink { get; set; } + public string GitLink { get; set; } + public string GitRef { get; set; } + public Image Image { get; internal set; } + + public string GetGithubLinkOf(string path) + { + return $"https://raw.githubusercontent.com/{GitLink.Substring("https://github.com/".Length)}/{GitRef}/{path}"; + } + } +} diff --git a/contrib/DockerFileBuildHelper/Extensions.cs b/contrib/DockerFileBuildHelper/Extensions.cs new file mode 100644 index 0000000..f645a48 --- /dev/null +++ b/contrib/DockerFileBuildHelper/Extensions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; +using YamlDotNet.RepresentationModel; + +namespace DockerFileBuildHelper +{ + public static class Extensions + { + public static YamlNode TryGet(this YamlNode node, string key) + { + try + { + return node[key]; + } + catch (KeyNotFoundException) { return null; } + } + } +} diff --git a/contrib/DockerFileBuildHelper/Image.cs b/contrib/DockerFileBuildHelper/Image.cs new file mode 100644 index 0000000..7733b79 --- /dev/null +++ b/contrib/DockerFileBuildHelper/Image.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; + +namespace DockerFileBuildHelper +{ + public class Image + { + public string User { get; private set; } + public string Name { get; private set; } + public string Tag { get; private set; } + + public string DockerHubLink + { + get + { + return User == string.Empty ? + $"https://hub.docker.com/_/{Name}" : + $"https://hub.docker.com/r/{User}/{Name}"; + } + } + + public static Image Parse(string str) + { + //${BTCPAY_IMAGE: -btcpayserver / btcpayserver:1.0.3.21} + var variableMatch = Regex.Match(str, @"\$\{[^-]+-([^\}]+)\}"); + if(variableMatch.Success) + { + str = variableMatch.Groups[1].Value; + } + Image img = new Image(); + var match = Regex.Match(str, "([^/]*/)?([^:]+):?(.*)"); + if (!match.Success) + throw new FormatException(); + img.User = match.Groups[1].Length == 0 ? string.Empty : match.Groups[1].Value.Substring(0, match.Groups[1].Value.Length - 1); + img.Name = match.Groups[2].Value; + img.Tag = match.Groups[3].Value; + if (img.Tag == string.Empty) + img.Tag = "latest"; + return img; + } + + public override string ToString() + { + StringBuilder builder = new StringBuilder(); + if (!String.IsNullOrWhiteSpace(User)) + builder.Append($"{User}/"); + builder.Append($"{Name}"); + if (!String.IsNullOrWhiteSpace(Tag)) + builder.Append($":{Tag}"); + return builder.ToString(); + } + } +} diff --git a/contrib/DockerFileBuildHelper/Program.cs b/contrib/DockerFileBuildHelper/Program.cs new file mode 100644 index 0000000..bd8cde0 --- /dev/null +++ b/contrib/DockerFileBuildHelper/Program.cs @@ -0,0 +1,271 @@ +using System; +using YamlDotNet; +using YamlDotNet.Helpers; +using System.Linq; +using System.Collections.Generic; +using System.IO; +using YamlDotNet.RepresentationModel; +using YamlDotNet.Serialization; +using System.Net.Http; +using System.Threading.Tasks; +using System.Text; + +namespace DockerFileBuildHelper +{ + class Program + { + static int Main(string[] args) + { + string outputFile = null; + for (int i = 0; i < args.Length; i++) + { + if (args[i] == "-o") + outputFile = args[i + 1]; + } + return new Program().Run(outputFile) ? 0 : 1; + } + + private bool Run(string outputFile) + { + var fragmentDirectory = Path.GetFullPath(Path.Combine(FindRoot("contrib"), "..", "docker-compose-generator", "docker-fragments")); + List> downloading = new List>(); + List dockerInfos = new List(); + foreach(var image in new[] + { + Image.Parse("btcpayserver/docker-compose-generator"), + Image.Parse("btcpayserver/docker-compose-builder:1.23.2"), + }.Concat(GetImages(fragmentDirectory))) + { + Console.WriteLine($"Image: {image.ToString()}"); + var info = GetDockerInfo(image); + if (info == null) + { + Console.WriteLine($"Missing image info: {image}"); + return false; + } + dockerInfos.Add(info); + downloading.Add(CheckLink(info, info.DockerFilePath)); + downloading.Add(CheckLink(info, info.DockerFilePathARM32v7)); + downloading.Add(CheckLink(info, info.DockerFilePathARM64v8)); + } + + Task.WaitAll(downloading.ToArray()); + var canDownloadEverything = downloading.All(o => o.Result); + if (!canDownloadEverything) + return false; + StringBuilder builder = new StringBuilder(); + builder.AppendLine("#!/bin/bash"); + builder.AppendLine(); + builder.AppendLine("# This file is automatically generated by the DockerFileBuildHelper tool, run DockerFileBuildHelper/update-repo.sh to update it"); + builder.AppendLine("set -e"); + foreach (var info in dockerInfos) + { + builder.AppendLine($"# Build {info.Image.Name}"); + builder.AppendLine($"git clone {info.GitLink} {info.Image.Name}"); + var dockerFile = DockerFile.Parse($"{info.DockerFilePath ?? info.DockerFilePathARM32v7 ?? info.DockerFilePathARM64v8}"); + builder.AppendLine($"cd {info.Image.Name}"); + builder.AppendLine($"git checkout {info.GitRef}"); + builder.AppendLine($"cd {dockerFile.DockerFilePath}"); + builder.AppendLine($"docker build -f \"{dockerFile.DockerFileName}\" -t \"{info.Image}\" ."); + builder.AppendLine($"cd - && cd .."); + builder.AppendLine(); + builder.AppendLine(); + } + var script = builder.ToString().Replace("\r\n", "\n"); + if (string.IsNullOrEmpty(outputFile)) + outputFile = "build-all.sh"; + File.WriteAllText(outputFile, script); + Console.WriteLine($"Generated file \"{Path.GetFullPath(outputFile)}\""); + return true; + } + HttpClient client = new HttpClient(); + private async Task CheckLink(DockerInfo info, string path) + { + if (path == null) + return true; + var link = info.GetGithubLinkOf(path); + var resp = await client.GetAsync(link); + if(!resp.IsSuccessStatusCode) + { + Console.WriteLine($"\tBroken link detected for image {info.Image} ({link})"); + return false; + } + return true; + } + + private IEnumerable GetImages(string fragmentDirectory) + { + var deserializer = new DeserializerBuilder().Build(); + var serializer = new SerializerBuilder().Build(); + foreach (var file in Directory.EnumerateFiles(fragmentDirectory, "*.yml")) + { + var root = ParseDocument(file); + if (root.TryGet("services") == null) + continue; + foreach (var service in ((YamlMappingNode)root["services"]).Children) + { + var imageStr = service.Value.TryGet("image"); + if (imageStr == null) + continue; + var image = Image.Parse(imageStr.ToString()); + yield return image; + } + } + } + private DockerInfo GetDockerInfo(Image image) + { + DockerInfo dockerInfo = new DockerInfo(); + switch (image.Name) + { + case "btglnd": + dockerInfo.DockerFilePath = "BTCPayServer.Dockerfile"; + dockerInfo.GitLink = "https://github.com/vutov/lnd"; + dockerInfo.GitRef = "master"; + break; + case "docker-compose-builder": + dockerInfo.DockerFilePathARM32v7 = "linuxarm32v7.Dockerfile"; + dockerInfo.GitLink = "https://github.com/btcpayserver/docker-compose-builder"; + dockerInfo.GitRef = $"v{image.Tag}"; + break; + case "docker-compose-generator": + dockerInfo.DockerFilePath = "docker-compose-generator/linuxamd64.Dockerfile"; + dockerInfo.DockerFilePathARM32v7 = "docker-compose-generator/linuxarm32v7.Dockerfile"; + dockerInfo.GitLink = "https://github.com/btcpayserver/btcpayserver-docker"; + dockerInfo.GitRef = $"dcg-latest"; + break; + case "docker-bitcoingold": + dockerInfo.DockerFilePath = $"bitcoingold/{image.Tag}/Dockerfile"; + dockerInfo.GitLink = "https://github.com/Vutov/docker-bitcoin"; + dockerInfo.GitRef = "master"; + break; + case "clightning": + dockerInfo.DockerFilePath = $"Dockerfile"; + dockerInfo.GitLink = "https://github.com/NicolasDorier/lightning"; + dockerInfo.GitRef = $"basedon-{image.Tag}"; + break; + case "lnd": + dockerInfo.DockerFilePath = "BTCPayServer.Dockerfile"; + dockerInfo.GitLink = "https://github.com/btcpayserver/lnd"; + dockerInfo.GitRef = $"basedon-v{image.Tag}"; + break; + case "bitcoin": + dockerInfo.DockerFilePath = $"Bitcoin/{image.Tag}/linuxamd64.Dockerfile"; + dockerInfo.DockerFilePathARM32v7 = $"Bitcoin/{image.Tag}/linuxarm32v7.Dockerfile"; + dockerInfo.DockerFilePathARM64v8 = $"Bitcoin/{image.Tag}/linuxarm64v8.Dockerfile"; + dockerInfo.GitLink = "https://github.com/btcpayserver/dockerfile-deps"; + dockerInfo.GitRef = $"Bitcoin/{image.Tag}"; + break; + case "btcpayserver": + dockerInfo.DockerFilePath = "Dockerfile.linuxamd64"; + dockerInfo.DockerFilePathARM32v7 = "Dockerfile.linuxarm32v7"; + dockerInfo.GitLink = "https://github.com/btcpayserver/btcpayserver"; + dockerInfo.GitRef = $"v{image.Tag}"; + break; + case "nbxplorer": + dockerInfo.DockerFilePath = "Dockerfile.linuxamd64"; + dockerInfo.DockerFilePathARM32v7 = "Dockerfile.linuxarm32v7"; + dockerInfo.GitLink = "https://github.com/dgarage/nbxplorer"; + dockerInfo.GitRef = $"v{image.Tag}"; + break; + case "dogecoin": + dockerInfo.DockerFilePath = $"dogecoin/{image.Tag}/Dockerfile"; + dockerInfo.GitLink = "https://github.com/rockstardev/docker-bitcoin"; + dockerInfo.GitRef = "feature/dogecoin"; + break; + case "docker-feathercoin": + dockerInfo.DockerFilePath = $"feathercoin/{image.Tag}/Dockerfile"; + dockerInfo.GitLink = "https://github.com/ChekaZ/docker"; + dockerInfo.GitRef = "master"; + break; + case "docker-groestlcoin": + dockerInfo.DockerFilePath = $"groestlcoin/{image.Tag}/Dockerfile"; + dockerInfo.GitLink = "https://github.com/NicolasDorier/docker-bitcoin"; + dockerInfo.GitRef = "master"; + break; + case "docker-viacoin": + dockerInfo.DockerFilePath = $"viacoin/{image.Tag}/docker-viacoin"; + dockerInfo.GitLink = "https://github.com/viacoin/docker-viacoin"; + dockerInfo.GitRef = "master"; + break; + case "docker-litecoin": + dockerInfo.DockerFilePath = $"litecoin/{image.Tag}/Dockerfile"; + dockerInfo.GitLink = "https://github.com/NicolasDorier/docker-bitcoin"; + dockerInfo.GitRef = "master"; + break; + case "docker-monacoin": + dockerInfo.DockerFilePath = $"monacoin/{image.Tag}/Dockerfile"; + dockerInfo.GitLink = "https://github.com/wakiyamap/docker-bitcoin"; + dockerInfo.GitRef = "master"; + break; + case "nginx": + dockerInfo.DockerFilePath = $"stable/stretch/Dockerfile"; + dockerInfo.GitLink = "https://github.com/nginxinc/docker-nginx"; + dockerInfo.GitRef = $"master"; + break; + case "docker-gen": + dockerInfo.DockerFilePath = $"linuxamd64.Dockerfile"; + dockerInfo.DockerFilePathARM32v7 = $"linuxarm32v7.Dockerfile"; + dockerInfo.GitLink = "https://github.com/btcpayserver/docker-gen"; + dockerInfo.GitRef = $"v{image.Tag}"; + break; + case "letsencrypt-nginx-proxy-companion": + dockerInfo.DockerFilePath = $"linuxamd64.Dockerfile"; + dockerInfo.DockerFilePathARM32v7 = $"linuxarm32v7.Dockerfile"; + dockerInfo.GitLink = "https://github.com/btcpayserver/docker-letsencrypt-nginx-proxy-companion"; + dockerInfo.GitRef = $"v{image.Tag}"; + break; + case "postgres": + dockerInfo.DockerFilePath = $"9.6/Dockerfile"; + dockerInfo.DockerFilePathARM32v7 = $"9.6/Dockerfile"; + dockerInfo.GitLink = "https://github.com/docker-library/postgres"; + dockerInfo.GitRef = $"b7cb3c6eacea93be2259381033be3cc435649369"; + break; + case "traefik": + dockerInfo.DockerFilePath = $"scratch/amd64/Dockerfile"; + dockerInfo.DockerFilePathARM32v7 = $"scratch/arm/Dockerfile"; + dockerInfo.GitLink = "https://github.com/containous/traefik-library-image"; + dockerInfo.GitRef = $"master"; + break; + default: + return null; + } + dockerInfo.DockerHubLink = image.DockerHubLink; + dockerInfo.Image = image; + return dockerInfo; + } + + private YamlMappingNode ParseDocument(string fragment) + { + var input = new StringReader(File.ReadAllText(fragment)); + YamlStream stream = new YamlStream(); + stream.Load(input); + return (YamlMappingNode)stream.Documents[0].RootNode; + } + + private static void DeleteDirectory(string outputDirectory) + { + try + { + Directory.Delete(outputDirectory, true); + } + catch + { + } + } + + private static string FindRoot(string rootDirectory) + { + string directory = Directory.GetCurrentDirectory(); + int i = 0; + while (true) + { + if (i > 10) + throw new DirectoryNotFoundException(rootDirectory); + if (directory.EndsWith(rootDirectory)) + return directory; + directory = Path.GetFullPath(Path.Combine(directory, "..")); + i++; + } + } + } +} diff --git a/contrib/DockerFileBuildHelper/README.md b/contrib/DockerFileBuildHelper/README.md new file mode 100644 index 0000000..f1b1d43 --- /dev/null +++ b/contrib/DockerFileBuildHelper/README.md @@ -0,0 +1,16 @@ +# DockerFile build helper + +By default, when you use docker deployment, you are fetching pre-built docker images hosted on dockerhub. +While this bring the advantage that deployment is fast and reliable, this also mean that you are ultimately trusting the owner of the docker images. +This repository generate a script that you can use to build all images from the sources by yourself. + +## How to use? + +Install [.NET Core SDK](https://dotnet.microsoft.com/download) and run: + +```bash +./run.sh +``` + +This will build a `build-all.sh` file. +Note that the diff --git a/contrib/DockerFileBuildHelper/run.sh b/contrib/DockerFileBuildHelper/run.sh new file mode 100755 index 0000000..c2bba29 --- /dev/null +++ b/contrib/DockerFileBuildHelper/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +dotnet run --no-launch-profile -c Release -- $@ \ No newline at end of file diff --git a/contrib/DockerFileBuildHelper/update-repo.ps1 b/contrib/DockerFileBuildHelper/update-repo.ps1 new file mode 100755 index 0000000..d90ded0 --- /dev/null +++ b/contrib/DockerFileBuildHelper/update-repo.ps1 @@ -0,0 +1 @@ +dotnet run --no-launch-profile -c Release -- -o "../build-all-images.sh" \ No newline at end of file diff --git a/contrib/DockerFileBuildHelper/update-repo.sh b/contrib/DockerFileBuildHelper/update-repo.sh new file mode 100755 index 0000000..c50753c --- /dev/null +++ b/contrib/DockerFileBuildHelper/update-repo.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +dotnet run --no-launch-profile -c Release -- -o "../build-all-images.sh" \ No newline at end of file diff --git a/contrib/build-all-images.sh b/contrib/build-all-images.sh new file mode 100755 index 0000000..0877848 --- /dev/null +++ b/contrib/build-all-images.sh @@ -0,0 +1,202 @@ +#!/bin/bash + +# This file is automatically generated by the DockerFileBuildHelper tool, run DockerFileBuildHelper/update-repo.sh to update it +set -e +# Build docker-compose-generator +git clone https://github.com/btcpayserver/btcpayserver-docker docker-compose-generator +cd docker-compose-generator +git checkout dcg-latest +cd docker-compose-generator +docker build -f "linuxamd64.Dockerfile" -t "btcpayserver/docker-compose-generator:latest" . +cd - && cd .. + + +# Build docker-compose-builder +git clone https://github.com/btcpayserver/docker-compose-builder docker-compose-builder +cd docker-compose-builder +git checkout v1.23.2 +cd . +docker build -f "linuxarm32v7.Dockerfile" -t "btcpayserver/docker-compose-builder:1.23.2" . +cd - && cd .. + + +# Build btglnd +git clone https://github.com/vutov/lnd btglnd +cd btglnd +git checkout master +cd . +docker build -f "BTCPayServer.Dockerfile" -t "kamigawabul/btglnd:latest" . +cd - && cd .. + + +# Build docker-bitcoingold +git clone https://github.com/Vutov/docker-bitcoin docker-bitcoingold +cd docker-bitcoingold +git checkout master +cd bitcoingold/0.15.2 +docker build -f "Dockerfile" -t "kamigawabul/docker-bitcoingold:0.15.2" . +cd - && cd .. + + +# Build clightning +git clone https://github.com/NicolasDorier/lightning clightning +cd clightning +git checkout basedon-v0.6.2-3 +cd . +docker build -f "Dockerfile" -t "nicolasdorier/clightning:v0.6.2-3" . +cd - && cd .. + + +# Build lnd +git clone https://github.com/btcpayserver/lnd lnd +cd lnd +git checkout basedon-v0.5-beta-2 +cd . +docker build -f "BTCPayServer.Dockerfile" -t "btcpayserver/lnd:0.5-beta-2" . +cd - && cd .. + + +# Build bitcoin +git clone https://github.com/btcpayserver/dockerfile-deps bitcoin +cd bitcoin +git checkout Bitcoin/0.17.0 +cd Bitcoin/0.17.0 +docker build -f "linuxamd64.Dockerfile" -t "btcpayserver/bitcoin:0.17.0" . +cd - && cd .. + + +# Build btcpayserver +git clone https://github.com/btcpayserver/btcpayserver btcpayserver +cd btcpayserver +git checkout v1.0.3.21 +cd . +docker build -f "Dockerfile.linuxamd64" -t "btcpayserver/btcpayserver:1.0.3.21" . +cd - && cd .. + + +# Build dogecoin +git clone https://github.com/rockstardev/docker-bitcoin dogecoin +cd dogecoin +git checkout feature/dogecoin +cd dogecoin/1.10.0 +docker build -f "Dockerfile" -t "rockstardev/dogecoin:1.10.0" . +cd - && cd .. + + +# Build docker-feathercoin +git clone https://github.com/ChekaZ/docker docker-feathercoin +cd docker-feathercoin +git checkout master +cd feathercoin/0.16.3 +docker build -f "Dockerfile" -t "chekaz/docker-feathercoin:0.16.3" . +cd - && cd .. + + +# Build docker-groestlcoin +git clone https://github.com/NicolasDorier/docker-bitcoin docker-groestlcoin +cd docker-groestlcoin +git checkout master +cd groestlcoin/2.16.3 +docker build -f "Dockerfile" -t "nicolasdorier/docker-groestlcoin:2.16.3" . +cd - && cd .. + + +# Build clightning +git clone https://github.com/NicolasDorier/lightning clightning +cd clightning +git checkout basedon-v0.6.2-3 +cd . +docker build -f "Dockerfile" -t "nicolasdorier/clightning:v0.6.2-3" . +cd - && cd .. + + +# Build lnd +git clone https://github.com/btcpayserver/lnd lnd +cd lnd +git checkout basedon-v0.5-beta-2 +cd . +docker build -f "BTCPayServer.Dockerfile" -t "btcpayserver/lnd:0.5-beta-2" . +cd - && cd .. + + +# Build docker-litecoin +git clone https://github.com/NicolasDorier/docker-bitcoin docker-litecoin +cd docker-litecoin +git checkout master +cd litecoin/0.16.3 +docker build -f "Dockerfile" -t "nicolasdorier/docker-litecoin:0.16.3" . +cd - && cd .. + + +# Build docker-monacoin +git clone https://github.com/wakiyamap/docker-bitcoin docker-monacoin +cd docker-monacoin +git checkout master +cd monacoin/0.16.3 +docker build -f "Dockerfile" -t "wakiyamap/docker-monacoin:0.16.3" . +cd - && cd .. + + +# Build nbxplorer +git clone https://github.com/dgarage/nbxplorer nbxplorer +cd nbxplorer +git checkout v2.0.0.1 +cd . +docker build -f "Dockerfile.linuxamd64" -t "nicolasdorier/nbxplorer:2.0.0.1" . +cd - && cd .. + + +# Build nginx +git clone https://github.com/nginxinc/docker-nginx nginx +cd nginx +git checkout master +cd stable/stretch +docker build -f "Dockerfile" -t "nginx:stable" . +cd - && cd .. + + +# Build docker-gen +git clone https://github.com/btcpayserver/docker-gen docker-gen +cd docker-gen +git checkout v0.7.4 +cd . +docker build -f "linuxamd64.Dockerfile" -t "btcpayserver/docker-gen:0.7.4" . +cd - && cd .. + + +# Build letsencrypt-nginx-proxy-companion +git clone https://github.com/btcpayserver/docker-letsencrypt-nginx-proxy-companion letsencrypt-nginx-proxy-companion +cd letsencrypt-nginx-proxy-companion +git checkout v1.10.0 +cd . +docker build -f "linuxamd64.Dockerfile" -t "btcpayserver/letsencrypt-nginx-proxy-companion:1.10.0" . +cd - && cd .. + + +# Build postgres +git clone https://github.com/docker-library/postgres postgres +cd postgres +git checkout b7cb3c6eacea93be2259381033be3cc435649369 +cd 9.6 +docker build -f "Dockerfile" -t "postgres:9.6.5" . +cd - && cd .. + + +# Build traefik +git clone https://github.com/containous/traefik-library-image traefik +cd traefik +git checkout master +cd scratch/amd64 +docker build -f "Dockerfile" -t "traefik:latest" . +cd - && cd .. + + +# Build docker-viacoin +git clone https://github.com/viacoin/docker-viacoin docker-viacoin +cd docker-viacoin +git checkout master +cd viacoin/0.15.2 +docker build -f "docker-viacoin" -t "romanornr/docker-viacoin:0.15.2" . +cd - && cd .. + +