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));
}
}