using System.Runtime.Versioning; using System.Text.RegularExpressions; namespace ReleaseBuilder; /// /// Static methods for working with environment variables /// public static class EnvHelper { /// /// Reads the environment key, and expands environment variables inside. /// If no key is found, the default value is returned /// /// The key to use /// The default value if the key is not set /// The expanded string public static string ExpandEnv(string key, string defaultValue) { var value = Environment.GetEnvironmentVariable(key); if (string.IsNullOrWhiteSpace(value)) value = defaultValue ?? string.Empty; // Bash-style env expansion "${name}", done after normal env expansion return Regex.Replace(Environment.ExpandEnvironmentVariables(value), "\\${(?[^}]+)}", m => Environment.GetEnvironmentVariable(m.Groups["name"].Value) ?? string.Empty ); } /// /// Reads the environment key, and expands environment variables inside. /// If no key is found, the default value is returned /// /// The key to use /// The default value if the key is not set /// The value public static string GetEnvKey(string key, string defaultValue) { var value = Environment.GetEnvironmentVariable(key); if (string.IsNullOrWhiteSpace(value)) value = defaultValue ?? string.Empty; return value; } /// /// Returns an executable path /// /// The path to expand /// The executable path public static string GetExecutablePath(string path) => string.IsNullOrWhiteSpace(path) ? path : OperatingSystem.IsWindows() ? Path.ChangeExtension(path, ".exe") : path; /// /// Returns a value if the path is executable /// /// The path to execute /// true if the path is executable; false otherwise public static bool IsExecutable(string path) { if (!File.Exists(path)) return false; if (OperatingSystem.IsWindows()) return path.EndsWith(".exe"); return File.GetUnixFileMode(path).HasFlag(UnixFileMode.OtherExecute); } /// /// Attempts to find the executable with the given name /// /// The command name /// The env key for overrides /// The default value /// The command, or null public static string? FindCommand(string command, string? envkey, string? defaultValue = null) { if (!string.IsNullOrWhiteSpace(envkey)) { var target = GetExecutablePath(ExpandEnv(envkey, "")); if (!string.IsNullOrWhiteSpace(target)) { if (!File.Exists(target)) throw new Exception($"Executable specified for {envkey} but not found: {target}"); if (!IsExecutable(target)) throw new Exception($"File specified for {envkey} found but is not executable: {target}"); return target; } } var folders = (Environment.GetEnvironmentVariable("PATH") ?? string.Empty).Split(Path.PathSeparator); return folders .Where(x => !string.IsNullOrWhiteSpace(x)) .Select(x => GetExecutablePath(Path.Combine(x, command))) .FirstOrDefault(IsExecutable) ?? defaultValue; } /// /// Copies the contents of into . /// The must exist, and the contents are copied, not the folder itself. /// The can exist, in which case the contents are not deleted, but overwritten (merged) /// /// The directory to copy /// /// /// public static void CopyDirectory(string sourceDir, string targetPath, bool recursive) { if (!Directory.Exists(sourceDir)) throw new Exception($"Directory is missing: {sourceDir}"); if (!Directory.Exists(targetPath)) Directory.CreateDirectory(targetPath); foreach (var f in Directory.EnumerateFileSystemEntries(sourceDir, "*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)) { if (File.Exists(f)) File.Copy(f, Path.Combine(targetPath, Path.GetRelativePath(sourceDir, f)), true); else if (recursive && Directory.Exists(f)) { var tg = Path.Combine(Path.Combine(targetPath, Path.GetRelativePath(sourceDir, f))); if (!Directory.Exists(tg)) Directory.CreateDirectory(tg); } } } /// /// Changes ownership of the path to the user and group /// /// The path to operate on /// The user to change to /// The group to change to /// If the operation should be recursive /// An awaitable task [UnsupportedOSPlatform("windows")] public static Task Chown(string path, string user, string group, bool recursive) // TODO: Requires sudo, and the Docker workaround does not work on MacOS => Task.CompletedTask; /// /// Changes ownership of the path to the user and group /// /// The path to operate on /// The user to change to /// The group to change to /// If the operation should be recursive /// An awaitable task [UnsupportedOSPlatform("windows")] private static async Task ChownWitDocker(string path, string user, string group, bool recursive) { // Get the numeric UID and GID for use in Docker var uid = int.Parse(await ProcessHelper.ExecuteWithOutput(new[] { "id", "-u", user })); var gid = int.Parse( OperatingSystem.IsMacOS() ? (await ProcessHelper.ExecuteWithOutput(["dscl", ".", "-read", $"/Groups/{group}", "PrimaryGroupID"])).Trim().Split(":", 2)[1].Trim() : (await ProcessHelper.ExecuteWithOutput(new[] { "getent", "group", group })).Trim().Split(":", 3)[2] ); var baseFolder = Path.GetDirectoryName(path); var targetEntry = Path.GetFileName(path); // Use docker to set the ownership await ProcessHelper.Execute(new[] { "docker", "run", "--mount", $"type=bind,source={baseFolder},target=/opt/mount", "alpine:latest", "chown", recursive ? "-R" : "", $"{uid}:{gid}", Path.Combine("/opt/mount", targetEntry) }); } /// /// Returns the unix file mode pattern represented by the mode string /// /// The unix mode string, e.g. "+x" /// The unix file mode public static UnixFileMode GetUnixFileMode(string modestr) { var current = UnixFileMode.None; var mmatch = Regex.Match(modestr, @"^((?[augo]{0,3})(?\+|\-)(?[rwx]{1,3}))$", RegexOptions.IgnoreCase | RegexOptions.Compiled); if (!mmatch.Success) throw new Exception($"Invalid mode string: {modestr}"); var who = mmatch.Groups["who"].Value.ToLowerInvariant(); if (string.IsNullOrWhiteSpace(who) || who.Contains('a')) who = "ugo"; var op = mmatch.Groups["op"].Value; var mode = mmatch.Groups["mode"].Value.ToLowerInvariant(); foreach (var m in mode) foreach (var w in who) { var p = $"{w}{m}" switch { "ur" => UnixFileMode.UserRead, "uw" => UnixFileMode.UserWrite, "ux" => UnixFileMode.UserExecute, "gr" => UnixFileMode.GroupRead, "gw" => UnixFileMode.GroupWrite, "gx" => UnixFileMode.GroupExecute, "or" => UnixFileMode.OtherRead, "ow" => UnixFileMode.OtherWrite, "ox" => UnixFileMode.OtherExecute, _ => throw new Exception("Unsupported bitflag combo") }; current |= p; } return current; } /// /// Helper function to add unix filemode bits /// /// The path to operate on (must exist) /// The unix file mode [UnsupportedOSPlatform("windows")] public static void AddFilemode(string path, UnixFileMode mode) => File.SetUnixFileMode(path, File.GetUnixFileMode(path) | mode); /// /// Helper function to remove unix filemode bits /// /// The path to operate on (must exist) /// The unix file mode [UnsupportedOSPlatform("windows")] public static void RemoveFilemode(string path, UnixFileMode mode) => File.SetUnixFileMode(path, File.GetUnixFileMode(path) & ~mode); /// /// Helper function to set unix filemode /// /// The path to operate on (must exist) /// The unix mode string, e.g. "+x" [UnsupportedOSPlatform("windows")] public static void SetFilemode(string path, string modestr) { if (modestr.Contains("+")) AddFilemode(path, GetUnixFileMode(modestr)); else RemoveFilemode(path, GetUnixFileMode(modestr)); } }