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