diff --git a/App.config b/App.config new file mode 100644 index 0000000..74ade9d --- /dev/null +++ b/App.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/CitizenMP.Server.Installer.csproj b/CitizenMP.Server.Installer.csproj new file mode 100644 index 0000000..8782ca9 --- /dev/null +++ b/CitizenMP.Server.Installer.csproj @@ -0,0 +1,89 @@ + + + + + + Debug + AnyCPU + {DDF5040E-9C6C-4686-800B-D4563C289F01} + Exe + Properties + CitizenMP.Server.Installer + citimp_upd + v4.0 + 512 + 4fcb3eee + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + CitizenMP.Server.Installer.Program + + + + False + ..\packages\CommandLineParser.1.9.71\lib\net40\CommandLine.dll + + + ..\packages\LibGit2Sharp.0.20.1.0\lib\net40\LibGit2Sharp.dll + + + + + + + + + ..\packages\Mono.Posix.4.0.0.0\lib\net40\Mono.Posix.dll + + + + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/CommandLineOptions.cs b/CommandLineOptions.cs new file mode 100644 index 0000000..14808d4 --- /dev/null +++ b/CommandLineOptions.cs @@ -0,0 +1,62 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using CommandLine; +using CommandLine.Text; +using Microsoft.Build.Framework; + +namespace CitizenMP.Server.Installer +{ + internal class CommandLineOptions + { + [Option('v', "verbosity", DefaultValue = LoggerVerbosity.Quiet, HelpText = "Sets the build output verbosity. Possible values: Minimal, Quiet, Normal, Detailed, Diagnostic")] + public LoggerVerbosity Verbosity { get; set; } + + [Option("source", DefaultValue = "src", HelpText = "Sets the path where the source files will be stored.")] + public string SourceDir { get; set; } + + [Option("log", DefaultValue = true, HelpText = "Write a log file \"build.log\" to the output folder.")] + public bool WriteLogFile { get; set; } + + [ValueOption(0)] + public string OutputPath { get; set; } + + [HelpOption] + public string GetUsage() + { + var programInfo = FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location); + var assembly = Assembly.GetExecutingAssembly(); + + var help = new HelpText + { + AddDashesToOption = true, + AdditionalNewLineAfterOption = true, + Copyright = programInfo.LegalCopyright, + Heading = new HeadingInfo(programInfo.ProductName, programInfo.ProductVersion), + MaximumDisplayWidth = Console.BufferWidth + }; + + var errors = help.RenderParsingErrorsText(this, 2); + if (!string.IsNullOrEmpty(errors)) + { + help.AddPreOptionsLine(string.Concat(Environment.NewLine, "ERROR(S):")); + help.AddPreOptionsLine(errors); + } + + help.AddPreOptionsLine(" "); + help.AddPreOptionsLine(((AssemblyLicenseAttribute)assembly + .GetCustomAttributes(typeof(AssemblyLicenseAttribute), false) + .Single()).Value.Trim()); + help.AddPreOptionsLine(" "); + help.AddPreOptionsLine(string.Format("{0}{1} [options...] \"\"", + Process.GetCurrentProcess().ProcessName, + new FileInfo(Assembly.GetExecutingAssembly().Location).Extension)); + + help.AddOptions(this); + + return help.ToString(); + } + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..c51adee --- /dev/null +++ b/Program.cs @@ -0,0 +1,317 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using CommandLine; +using LibGit2Sharp; +using Microsoft.Build.BuildEngine; +using Microsoft.Build.Evaluation; +using Microsoft.Build.Execution; +using Microsoft.Build.Framework; +using Mono.Unix.Native; +using UnixSyscall = Mono.Unix.Native.Syscall; + +namespace CitizenMP.Server.Installer +{ + internal static class Program + { + // TODO: Get rid of this if possible + internal static readonly Signature GitSignature = new Signature("Downloader", "downloader@localhost", + new DateTimeOffset()); + + private static void PrepareDirectory(DirectoryInfo sourcePath, DirectoryInfo targetPath) + { + if (!sourcePath.Exists) + { + throw new DirectoryNotFoundException("Source directory " + sourcePath.FullName + " does not exist."); + } + + targetPath.Create(); + + foreach (var file in sourcePath.EnumerateFiles()) + { + var overwrite = !file.Extension.Equals("yml", StringComparison.OrdinalIgnoreCase); + var target = Path.Combine(targetPath.FullName, file.Name); + + // If file exists and should not be directly overwritten, just remember the user to manually update the file as needed. + if (File.Exists(target) && !overwrite) + { + var oldTarget = target; + target += ".dist"; + Console.Error.WriteLine( + "WARNING: File {0} needs a manual update! Compare with {1} and rewrite your file.", oldTarget, + target); + } + + file.CopyTo(target, overwrite); + } + + foreach (var subdirectory in sourcePath.EnumerateDirectories()) + { + PrepareDirectory(subdirectory, targetPath.CreateSubdirectory(subdirectory.Name)); + } + } + + private static int Main(string[] args) + { + // Parse cmdline arguments + var options = new CommandLineOptions(); + //args = args.DefaultIfEmpty("--help").ToArray(); + if (!Parser.Default.ParseArgumentsStrict(args, options, () => { Environment.Exit(-2); })) + { + return -2; + } + + if (string.IsNullOrEmpty(options.OutputPath)) + { + Console.Error.WriteLine("ERROR: No output directory given."); + Console.Write(options.GetUsage()); + return -2; + } + + var sourceDirectory = new DirectoryInfo(options.SourceDir); + var dataSourceDirectory = sourceDirectory + // Who knows if this directory will somewhen cease to exist... + .CreateSubdirectory("CitizenMP.Server") + .CreateSubdirectory("data"); + var outputDirectory = new DirectoryInfo(options.OutputPath); + var binOutputDirectory = new DirectoryInfo(Path.Combine(outputDirectory.FullName, "bin")); + + // Do we even have a copy or do we need to clone? + if (!Repository.IsValid(sourceDirectory.FullName)) + { + if (sourceDirectory.Exists) + { + Console.WriteLine("Deleting source code folder..."); + sourceDirectory.Delete(true); + } + + Console.WriteLine("Cloning source code repository..."); + Repository.Clone("http://tohjo.ez.lv/citidev/citizenmp-server.git", sourceDirectory.FullName); + } + else + { + // Update working dir + Console.WriteLine("Updating source code..."); + using (var git = new Repository(sourceDirectory.FullName)) + { + //git.Network.Pull(GitSignature, new PullOptions()); + git.UpdateRepository("HEAD"); + } + } + + // Check if we need to update by parsing AssemblyConfigurationAttribute in server assembly. + // Should have a space-separated segment saying "CommitHash=". + if (binOutputDirectory.Exists) + { + var serverBins = binOutputDirectory + .EnumerateFiles("*Server.exe", SearchOption.TopDirectoryOnly) + .ToArray(); + if (serverBins.Any()) + { + var serverAssembly = Assembly.LoadFile(serverBins.First().FullName); + var configurationAttribs = serverAssembly.GetCustomAttributes(typeof(AssemblyConfigurationAttribute), false); + if (configurationAttribs.Any()) + { + var configurationAttrib = (AssemblyConfigurationAttribute)configurationAttribs.First(); + foreach (var commitHash in configurationAttrib.Configuration.Split(' ') + .Where(section => section.StartsWith("CommitHash=")) + .Select(section => section.Split('=').Last())) + { + using (var repo = new Repository(sourceDirectory.FullName)) + { + if (commitHash != repo.Head.Tip.Sha) + continue; + + // Yup, same commit. + Console.WriteLine("Server is already up-to-date!"); + return 0; + } + } + } + } + } + + // Get submodules + using (var git = new Repository(sourceDirectory.FullName)) + { + Console.WriteLine("Downloading dependencies..."); + git.UpdateSubmodules(); + } + + // Patch AssemblyInfo.cs to include commit hash in an AssemblyConfigurationAttribute + Console.WriteLine("Patching assembly information..."); + var assemblyGuidRegex = + new Regex( + @"^[\s]*\[assembly[\s]*:[\s]*Guid[\s]*\([\s]*(?[@]?)""(?.*?)""[\s]*\)[\s]*\][\s]*$", + RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.Multiline); + var assemblyConfigurationRegex = + new Regex( + @"^[\s]*\[assembly[\s]*:[\s]*AssemblyConfiguration[\s]*\([\s]*(?[@]?)""(?.*?)""[\s]*\)[\s]*\][\s]*$", + RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.Multiline); + foreach (var assemblyInfoFile in sourceDirectory + .EnumerateFiles("AssemblyInfo.cs", SearchOption.AllDirectories)) + { + var sourceCode = File.ReadAllText(assemblyInfoFile.FullName); + + // Parse GUID + var guid = assemblyGuidRegex.Match(sourceCode).Groups["oldValue"].Value; + if (!guid.Equals("b14ff4c2-a2e5-416b-ae79-4580cda4d9d1", StringComparison.OrdinalIgnoreCase)) + { + //Console.WriteLine("\tSkipping assembly info for GUID \"{0}\" ({1}).", guid, assemblyInfoFile.Directory); + continue; + } + //Console.WriteLine("\tPatching assembly info for GUID \"{0}\" ({1}).", guid, assemblyInfoFile.Directory); + + if (!assemblyConfigurationRegex.IsMatch(sourceCode)) + { + sourceCode += Environment.NewLine; + sourceCode += @"// Inserted by CitizenMP Server Updater for version comparison"; + sourceCode += @"[assembly: AssemblyConfiguration("""")]"; + } + + using (var git = new Repository(sourceDirectory.FullName)) + { + sourceCode = assemblyConfigurationRegex.Replace(sourceCode, + m => string.Format("[assembly: AssemblyConfiguration({0}\"{1}CommitHash={2}\")]", + m.Groups["verbatimPrefix"].Value, + m.Groups["oldValue"].Length > 0 + ? m.Groups["oldValue"].Value + " " + : "", + // ReSharper disable once AccessToDisposedClosure + git.Head.Tip.Sha)); + } + + File.WriteAllText(assemblyInfoFile.FullName, sourceCode); + } + + + // Build project + Console.WriteLine("Building server binaries..."); + var slnPath = sourceDirectory.EnumerateFiles("*.sln", SearchOption.TopDirectoryOnly) + .First().FullName; + outputDirectory.Create(); + if (Build(slnPath, new Dictionary + { + {"Configuration", "Release"}, + {"Platform", "Any CPU"}, + {"DebugType", "None"}, + {"DebugSymbols", false.ToString()}, + {"OutputPath", binOutputDirectory.FullName}, + {"AllowedReferenceRelatedFileExtensions", "\".mdb\"=\"\";\".pdb\"=\"\";\".xml\"=\"\""} + }, Path.Combine(outputDirectory.FullName, "build.log")).OverallResult == BuildResultCode.Failure) + { + Console.Error.WriteLine("Build failed!"); + return 1; + } + + // Prepare with default files + PrepareDirectory(dataSourceDirectory, outputDirectory); + + // Write startup scripts + switch (Environment.OSVersion.Platform) + { + case PlatformID.Unix: + case PlatformID.MacOSX: + { + var startScriptPath = Path.Combine(outputDirectory.FullName, "start.sh"); + File.WriteAllText( + startScriptPath, + string.Join( + Environment.NewLine, + @"#!/bin/bash", + @"", + @"# switch to the script directory", + @"cd ""$( dirname ""${BASH_SOURCE[0]}""", + @"", + @"# run with mono", + @"mono ""bin/" + binOutputDirectory.EnumerateFiles("*.exe").First().Name + @""" $@", + @"")); + + // TODO: Pretty sure there is an easier way to do a programmatical chmod +x + Stat stat; + FilePermissions perms; + if (UnixSyscall.stat(startScriptPath, out stat) != 0) + { + perms = FilePermissions.S_IRUSR | FilePermissions.S_IRGRP | FilePermissions.S_IROTH + | FilePermissions.S_IWUSR + | FilePermissions.S_IXUSR; + } + else + { + perms = stat.st_mode; + } + UnixSyscall.chmod(startScriptPath, + perms + | FilePermissions.S_IXUSR | FilePermissions.S_IXGRP | FilePermissions.S_IXOTH); + } + break; + case PlatformID.Win32NT: + case PlatformID.Win32Windows: + { + var startScriptPath = Path.Combine(outputDirectory.FullName, "start.bat"); + File.WriteAllText( + startScriptPath, + string.Join(Environment.NewLine, + "@echo off", + @"", + @":: switch to the script directory", + @"pushd ""%~dp0""", + @"", + @":: run", + @"""bin\" + binOutputDirectory.EnumerateFiles("*.exe").First().Name + @""" %*", + @"")); + } + break; + default: + Console.Error.WriteLine("WARNING: No startup script created. Platform not supported."); + break; + } + + Console.WriteLine("Done."); + return 0; + } + + private static BuildResult Build(string solutionFilePath, IDictionary buildProperties, + string logPath = null) + { + var pc = new ProjectCollection(); + pc.RegisterLogger(new ConsoleLogger(LoggerVerbosity.Minimal)); + + var buildReq = new BuildRequestData(solutionFilePath, buildProperties, null, new[] {"Build"}, null); + + // Save environment + var oldMonoInputOutputMapping = Environment.GetEnvironmentVariable("MONO_IOMAP"); + + // Mono compatibility + Environment.SetEnvironmentVariable("MONO_IOMAP", "all"); + + var result = BuildManager.DefaultBuildManager.Build( + new BuildParameters(pc) + { + Loggers = + new[] + { + logPath != null + ? new FileLogger + { + Parameters = + "logfile=" + logPath, + Verbosity = LoggerVerbosity.Detailed, + ShowSummary = true, + SkipProjectStartedText = true + } + : new ConsoleLogger(LoggerVerbosity.Quiet) + }, + MaxNodeCount = Environment.ProcessorCount + }, buildReq); + + // Restore environment + Environment.SetEnvironmentVariable("MONO_IOMAP", oldMonoInputOutputMapping); + + return result; + } + } +} \ No newline at end of file diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..e488877 --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,39 @@ +using System.Reflection; +using System.Runtime.InteropServices; +using CommandLine; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("CitizenMP Server Updater")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Carl Kittelberger (Icedream)")] +[assembly: AssemblyProduct("CitizenMP Server Updater")] +[assembly: AssemblyCopyright("© 2014-2015 Carl Kittelberger")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: AssemblyLicense("This is free software. You may redistribute copies of it under the terms of the MIT License .")] +[assembly: AssemblyUsage("Usage: citimp_upd.exe [options...] ")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("c543f116-7bd6-4295-abff-4f80458e8be1")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyFileVersion("1.0")] +[assembly: AssemblyInformationalVersion("1.0")] \ No newline at end of file diff --git a/RepositoryExtensions.cs b/RepositoryExtensions.cs new file mode 100644 index 0000000..ffa3eb9 --- /dev/null +++ b/RepositoryExtensions.cs @@ -0,0 +1,39 @@ +using System.IO; +using System.Linq; +using LibGit2Sharp; + +namespace CitizenMP.Server.Installer +{ + static class RepositoryExtensions + { + public static void UpdateSubmodules(this IRepository git) + { + foreach (var submodule in git.Submodules) + { + var subrepoPath = Path.Combine(git.Info.WorkingDirectory, submodule.Path); + if (!Repository.IsValid(subrepoPath)) + { + Directory.Delete(subrepoPath, true); + Repository.Clone(submodule.Url, subrepoPath); + } + + using (var subrepo = new Repository(subrepoPath)) + { + subrepo.UpdateRepository(submodule.HeadCommitId.Sha); + } + } + } + + public static void UpdateRepository(this IRepository git, string committishOrBranchSpec) + { + git.RemoveUntrackedFiles(); + git.Reset(ResetMode.Hard); + git.Fetch(git.Network.Remotes.First().Name, new FetchOptions + { + TagFetchMode = TagFetchMode.None + }); + // TODO: Check out correct branch if needed + git.Checkout(committishOrBranchSpec, new CheckoutOptions(), Program.GitSignature); + } + } +} diff --git a/packages.config b/packages.config new file mode 100644 index 0000000..ca3be71 --- /dev/null +++ b/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file