208 lines
6.6 KiB
C#
208 lines
6.6 KiB
C#
using Sdt.Config;
|
|
using Sdt.Runner;
|
|
|
|
namespace Sdt.Core;
|
|
|
|
public sealed class ActionRunner : IActionRunner
|
|
{
|
|
public async Task<RunResult> RunStepAsync(
|
|
WorkflowStep step,
|
|
string projectRoot,
|
|
Action<string, bool> onOutput,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(step.Action))
|
|
{
|
|
var scriptPath = ScriptLocator.FindHelperScript(projectRoot, "build.py");
|
|
if (scriptPath is null)
|
|
throw new InvalidOperationException("build.py not found in bundled scripts or project scripts directory.");
|
|
|
|
var actionArgs = new List<string>
|
|
{
|
|
scriptPath,
|
|
step.Action,
|
|
"--project-root",
|
|
projectRoot,
|
|
};
|
|
actionArgs.AddRange(step.ActionArgs);
|
|
|
|
return await ProcessRunner.RunAsync(
|
|
PythonResolver.ResolveExecutable(),
|
|
actionArgs,
|
|
projectRoot,
|
|
onOutput,
|
|
cancellationToken: cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(step.Command))
|
|
return new RunResult(0, TimeSpan.Zero);
|
|
|
|
var workingDir = Path.GetFullPath(Path.Combine(projectRoot, step.WorkingDir));
|
|
|
|
var pwshReroute = await TryRunLegacyPwshScriptViaPythonAsync(
|
|
step,
|
|
projectRoot,
|
|
workingDir,
|
|
onOutput,
|
|
cancellationToken).ConfigureAwait(false);
|
|
if (pwshReroute is not null)
|
|
return pwshReroute;
|
|
|
|
return await ProcessRunner.RunAsync(
|
|
step.Command,
|
|
step.Args,
|
|
workingDir,
|
|
onOutput,
|
|
cancellationToken: cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
private static async Task<RunResult?> TryRunLegacyPwshScriptViaPythonAsync(
|
|
WorkflowStep step,
|
|
string projectRoot,
|
|
string workingDir,
|
|
Action<string, bool> onOutput,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
if (!IsPowerShellCommand(step.Command))
|
|
return null;
|
|
|
|
var args = step.Args;
|
|
var fileIndex = FindArgIndex(args, "-File");
|
|
if (fileIndex < 0 || fileIndex + 1 >= args.Count)
|
|
return null;
|
|
|
|
var psScriptArg = args[fileIndex + 1];
|
|
if (!psScriptArg.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase))
|
|
return null;
|
|
|
|
var pyScriptPath = ResolvePythonScriptPath(projectRoot, workingDir, psScriptArg);
|
|
if (pyScriptPath is null)
|
|
return null;
|
|
|
|
var translated = TranslatePowerShellArgsToPython(args.Skip(fileIndex + 2));
|
|
var pythonArgs = new List<string> { pyScriptPath };
|
|
pythonArgs.AddRange(translated);
|
|
|
|
onOutput($"Legacy PowerShell target detected. Trying Python script first: {Path.GetFileName(pyScriptPath)}", false);
|
|
|
|
var pyRun = await ProcessRunner.RunAsync(
|
|
PythonResolver.ResolveExecutable(),
|
|
pythonArgs,
|
|
workingDir,
|
|
onOutput,
|
|
cancellationToken: cancellationToken).ConfigureAwait(false);
|
|
|
|
if (pyRun.Success)
|
|
return pyRun;
|
|
|
|
var psScriptPath = ResolveScriptPath(workingDir, psScriptArg);
|
|
if (psScriptPath is null || !File.Exists(psScriptPath))
|
|
return pyRun;
|
|
|
|
onOutput(
|
|
$"Python script failed (exit {pyRun.ExitCode}). Falling back to legacy PowerShell script: {psScriptArg}",
|
|
true);
|
|
return null;
|
|
}
|
|
|
|
private static string? ResolvePythonScriptPath(string projectRoot, string workingDir, string psScriptArg)
|
|
{
|
|
var pyArg = Path.ChangeExtension(psScriptArg, ".py");
|
|
var candidate = ResolveScriptPath(workingDir, pyArg);
|
|
if (candidate is not null && File.Exists(candidate))
|
|
return candidate;
|
|
|
|
var fileName = Path.GetFileName(pyArg);
|
|
return ScriptLocator.FindHelperScript(projectRoot, fileName);
|
|
}
|
|
|
|
private static string? ResolveScriptPath(string workingDir, string scriptArg)
|
|
{
|
|
if (Path.IsPathRooted(scriptArg))
|
|
return scriptArg;
|
|
return Path.GetFullPath(Path.Combine(workingDir, scriptArg));
|
|
}
|
|
|
|
private static bool IsPowerShellCommand(string? command)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(command))
|
|
return false;
|
|
|
|
var normalized = Path.GetFileNameWithoutExtension(command).ToLowerInvariant();
|
|
return normalized is "pwsh" or "powershell";
|
|
}
|
|
|
|
private static int FindArgIndex(IReadOnlyList<string> args, string name)
|
|
{
|
|
for (var i = 0; i < args.Count; i++)
|
|
{
|
|
if (string.Equals(args[i], name, StringComparison.OrdinalIgnoreCase))
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
private static List<string> TranslatePowerShellArgsToPython(IEnumerable<string> inputArgs)
|
|
{
|
|
var result = new List<string>();
|
|
var list = inputArgs.ToList();
|
|
|
|
for (var i = 0; i < list.Count; i++)
|
|
{
|
|
var token = list[i];
|
|
if (!token.StartsWith("-", StringComparison.Ordinal) || token == "-")
|
|
{
|
|
result.Add(token);
|
|
continue;
|
|
}
|
|
|
|
var key = token.TrimStart('-');
|
|
if (key.Length == 0)
|
|
continue;
|
|
|
|
var mapped = MapPowerShellParameter(key);
|
|
var nextIsValue = (i + 1) < list.Count && !list[i + 1].StartsWith("-", StringComparison.Ordinal);
|
|
result.Add(mapped);
|
|
if (nextIsValue)
|
|
{
|
|
result.Add(list[i + 1]);
|
|
i++;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static string MapPowerShellParameter(string key)
|
|
{
|
|
return key.ToLowerInvariant() switch
|
|
{
|
|
"tauribundles" => "--tauri-bundles",
|
|
"projectroot" => "--project-root",
|
|
"reporoot" => "--repo-root",
|
|
"outputzip" => "--output-zip",
|
|
"inputzip" => "--input-zip",
|
|
"workingdir" => "--working-dir",
|
|
"outputdir" => "--output-dir",
|
|
_ => "--" + ToKebabCase(key)
|
|
};
|
|
}
|
|
|
|
private static string ToKebabCase(string value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
return value.ToLowerInvariant();
|
|
|
|
var chars = new List<char>(value.Length + 4);
|
|
for (var i = 0; i < value.Length; i++)
|
|
{
|
|
var c = value[i];
|
|
if (char.IsUpper(c) && i > 0 && value[i - 1] != '-')
|
|
chars.Add('-');
|
|
chars.Add(char.ToLowerInvariant(c));
|
|
}
|
|
|
|
return new string(chars.ToArray());
|
|
}
|
|
}
|