627 lines
22 KiB
C#
627 lines
22 KiB
C#
using System.Text.Json;
|
|
|
|
namespace Sdt.Config;
|
|
|
|
public sealed record BootstrapScanResult(
|
|
string ProjectRoot,
|
|
string ProjectName,
|
|
IReadOnlyList<string> ToolFamilies,
|
|
string? NodeWorkingDir,
|
|
string? PythonRequirementsFile,
|
|
bool HasDockerCompose,
|
|
IReadOnlyList<string> RootHints);
|
|
|
|
public static class ConfigBootstrapper
|
|
{
|
|
private const int MaxScanDepth = 4;
|
|
|
|
private static readonly HashSet<string> ExcludedDirectories = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
".git",
|
|
"node_modules",
|
|
".venv",
|
|
"venv",
|
|
"bin",
|
|
"obj",
|
|
".idea",
|
|
".vscode",
|
|
"dist",
|
|
"build",
|
|
".sdt",
|
|
};
|
|
|
|
private static readonly string[] RequirementCandidates =
|
|
[
|
|
"requirements.txt",
|
|
"requirements-dev.txt",
|
|
"requirements_cpu_only.txt",
|
|
"requirements_gpu.txt",
|
|
];
|
|
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
WriteIndented = true
|
|
};
|
|
|
|
public static BootstrapScanResult Scan(string startDir)
|
|
{
|
|
var root = FindProjectRoot(startDir);
|
|
var toolFamilies = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
var rootHints = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
if (Directory.Exists(Path.Combine(root, ".git")))
|
|
{
|
|
toolFamilies.Add("git");
|
|
rootHints.Add(".git");
|
|
}
|
|
|
|
var hasTopLevelSln = Directory.EnumerateFiles(root, "*.sln", SearchOption.TopDirectoryOnly).Any();
|
|
var hasCsproj = hasTopLevelSln || EnumerateFilesBounded(root, "*.csproj", MaxScanDepth).Any();
|
|
if (hasCsproj)
|
|
{
|
|
toolFamilies.Add("dotnet");
|
|
rootHints.Add("*.sln");
|
|
}
|
|
|
|
var topLevelPackageJson = Path.Combine(root, "package.json");
|
|
var packageJson = File.Exists(topLevelPackageJson)
|
|
? topLevelPackageJson
|
|
: EnumerateFilesBounded(root, "package.json", MaxScanDepth)
|
|
.OrderBy(p => p.Length)
|
|
.FirstOrDefault();
|
|
string? nodeWorkingDir = null;
|
|
if (packageJson is not null)
|
|
{
|
|
toolFamilies.Add("node");
|
|
toolFamilies.Add("npm");
|
|
nodeWorkingDir = Path.GetRelativePath(root, Path.GetDirectoryName(packageJson)!);
|
|
rootHints.Add("package.json");
|
|
}
|
|
|
|
string? requirements = RequirementCandidates
|
|
.Select(name => Path.Combine(root, name))
|
|
.FirstOrDefault(File.Exists);
|
|
if (requirements is null)
|
|
{
|
|
var pyproject = Path.Combine(root, "pyproject.toml");
|
|
if (File.Exists(pyproject))
|
|
{
|
|
toolFamilies.Add("python");
|
|
rootHints.Add("pyproject.toml");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
toolFamilies.Add("python");
|
|
rootHints.Add(Path.GetFileName(requirements)!);
|
|
}
|
|
|
|
var hasCargo = File.Exists(Path.Combine(root, "Cargo.toml")) ||
|
|
EnumerateFilesBounded(root, "Cargo.toml", MaxScanDepth).Any();
|
|
if (hasCargo)
|
|
{
|
|
toolFamilies.Add("cargo");
|
|
rootHints.Add("Cargo.toml");
|
|
}
|
|
|
|
var hasTauri = File.Exists(Path.Combine(root, "tauri.conf.json")) ||
|
|
EnumerateFilesBounded(root, "tauri.conf.json", MaxScanDepth).Any();
|
|
if (hasTauri)
|
|
{
|
|
toolFamilies.Add("tauri");
|
|
toolFamilies.Add("cargo");
|
|
toolFamilies.Add("node");
|
|
toolFamilies.Add("npm");
|
|
rootHints.Add("tauri.conf.json");
|
|
}
|
|
|
|
var hasDockerCompose = File.Exists(Path.Combine(root, "docker-compose.yml")) ||
|
|
File.Exists(Path.Combine(root, "docker-compose.yaml"));
|
|
if (hasDockerCompose || File.Exists(Path.Combine(root, "Dockerfile")))
|
|
{
|
|
toolFamilies.Add("docker");
|
|
rootHints.Add(hasDockerCompose ? "docker-compose.yml" : "Dockerfile");
|
|
}
|
|
|
|
var scriptsDir = Path.Combine(root, "scripts");
|
|
if (Directory.Exists(scriptsDir) &&
|
|
Directory.EnumerateFiles(scriptsDir, "*.py", SearchOption.TopDirectoryOnly).Any())
|
|
{
|
|
toolFamilies.Add("python");
|
|
rootHints.Add("scripts");
|
|
}
|
|
|
|
if (rootHints.Count == 0)
|
|
rootHints.Add("devtool.json");
|
|
|
|
return new BootstrapScanResult(
|
|
ProjectRoot: root,
|
|
ProjectName: new DirectoryInfo(root).Name,
|
|
ToolFamilies: toolFamilies.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList(),
|
|
NodeWorkingDir: nodeWorkingDir,
|
|
PythonRequirementsFile: requirements is null ? null : Path.GetRelativePath(root, requirements),
|
|
HasDockerCompose: hasDockerCompose,
|
|
RootHints: rootHints.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList());
|
|
}
|
|
|
|
public static DevToolConfig BuildDefaultConfig(BootstrapScanResult scan)
|
|
{
|
|
var workflows = BuildWorkflows(scan).ToList();
|
|
|
|
var toolingTools = scan.ToolFamilies
|
|
.Select(t => new ToolInstallDefinition { Tool = t, PreferredInstallCommands = [] })
|
|
.ToList();
|
|
|
|
var toolchains = new ToolchainConfig
|
|
{
|
|
Python = scan.ToolFamilies.Contains("python", StringComparer.OrdinalIgnoreCase)
|
|
? new PythonToolchain
|
|
{
|
|
Executable = "python",
|
|
WindowsExecutable = "py",
|
|
VenvDir = ".venv",
|
|
Profiles = []
|
|
}
|
|
: null,
|
|
Node = scan.ToolFamilies.Contains("node", StringComparer.OrdinalIgnoreCase)
|
|
? new NodeToolchain
|
|
{
|
|
PackageManager = "npm",
|
|
WorkingDir = string.IsNullOrWhiteSpace(scan.NodeWorkingDir) ? "." : scan.NodeWorkingDir
|
|
}
|
|
: null
|
|
};
|
|
|
|
var debugProfiles = BuildDebugProfiles(scan).ToList();
|
|
|
|
return new DevToolConfig
|
|
{
|
|
Name = scan.ProjectName,
|
|
Version = "0.1.0",
|
|
Project = new ProjectMetadata
|
|
{
|
|
Type = "generic",
|
|
RootHints = scan.RootHints.ToList(),
|
|
Artifacts = ["bin", "obj", ".sdt/debug"]
|
|
},
|
|
Toolchains = toolchains,
|
|
Tooling = new ToolingConfig { Tools = toolingTools },
|
|
Workflows = workflows,
|
|
Debug = new DebugConfig
|
|
{
|
|
Profiles = debugProfiles,
|
|
Diagnostics = new DebugDiagnosticsOptions
|
|
{
|
|
Enabled = true,
|
|
OutputDir = ".sdt/debug",
|
|
IncludeAllEnv = false,
|
|
CaptureEnvKeys =
|
|
[
|
|
"SDT_LOG_LEVEL",
|
|
"DOTNET_CLI_HOME",
|
|
"NUGET_PACKAGES",
|
|
"PIP_CACHE_DIR",
|
|
"NVM_HOME",
|
|
"NVM_SYMLINK",
|
|
],
|
|
BundleOnFailure = true
|
|
}
|
|
},
|
|
Env =
|
|
[
|
|
new EnvVarDef
|
|
{
|
|
Key = "SDT_LOG_LEVEL",
|
|
Description = "CLI log verbosity",
|
|
DefaultValue = "information",
|
|
Options = ["trace", "debug", "information", "warning", "error", "critical"]
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
public static string ToJson(DevToolConfig config) => JsonSerializer.Serialize(config, JsonOptions) + Environment.NewLine;
|
|
|
|
public static string WriteDefaultConfig(string projectRoot, DevToolConfig config, bool overwrite = false)
|
|
{
|
|
var path = Path.Combine(projectRoot, "devtool.json");
|
|
if (File.Exists(path) && !overwrite)
|
|
throw new InvalidOperationException($"devtool.json already exists at {path}");
|
|
|
|
File.WriteAllText(path, ToJson(config));
|
|
return path;
|
|
}
|
|
|
|
private static IEnumerable<WorkflowDefinition> BuildWorkflows(BootstrapScanResult scan)
|
|
{
|
|
var has = new Func<string, bool>(tool => scan.ToolFamilies.Contains(tool, StringComparer.OrdinalIgnoreCase));
|
|
var scripts = DetectScriptHelpers(scan.ProjectRoot);
|
|
|
|
if (scripts.Contains("publish-sidecar.py") ||
|
|
scripts.Contains("publish-app.py") ||
|
|
scripts.Contains("publish-webgateway.py") ||
|
|
scripts.Contains("publish-output.py") ||
|
|
scripts.Contains("sync-output.py") ||
|
|
scripts.Contains("run-webgateway.py"))
|
|
{
|
|
foreach (var workflow in BuildScriptDrivenWorkflows(scripts))
|
|
yield return workflow;
|
|
}
|
|
|
|
var buildSteps = new List<WorkflowStep>();
|
|
if (has("dotnet")) buildSteps.Add(StepAction("dotnet-build", "dotnet build", "dotnet-build"));
|
|
if (has("npm")) buildSteps.Add(StepAction("npm-build", "npm run build", "npm-build", scan.NodeWorkingDir));
|
|
if (has("cargo")) buildSteps.Add(StepAction("cargo-build", "cargo build", "cargo-build"));
|
|
if (has("tauri")) buildSteps.Add(StepAction("tauri-build", "tauri build", "tauri-build", scan.NodeWorkingDir ?? "."));
|
|
if (buildSteps.Count > 0)
|
|
{
|
|
yield return new WorkflowDefinition
|
|
{
|
|
Id = "build",
|
|
Label = "Build",
|
|
Description = "Build detected project stacks",
|
|
Group = "Build",
|
|
Steps = buildSteps
|
|
};
|
|
}
|
|
|
|
var depsSteps = new List<WorkflowStep>();
|
|
if (has("dotnet")) depsSteps.Add(StepAction("dotnet-restore", "dotnet restore", "dotnet-restore"));
|
|
if (has("npm")) depsSteps.Add(StepAction("npm-ci", "npm ci", "npm-ci", scan.NodeWorkingDir));
|
|
if (has("python") && !string.IsNullOrWhiteSpace(scan.PythonRequirementsFile))
|
|
{
|
|
depsSteps.Add(StepAction("python-pip-sync", "python pip sync", "python-pip-sync", ".", ["--requirements", scan.PythonRequirementsFile!]));
|
|
}
|
|
if (depsSteps.Count > 0)
|
|
{
|
|
yield return new WorkflowDefinition
|
|
{
|
|
Id = "deps-refresh",
|
|
Label = "Refresh Dependencies",
|
|
Description = "Restore/install dependency stacks",
|
|
Group = "Deps",
|
|
Steps = depsSteps
|
|
};
|
|
}
|
|
|
|
var testSteps = new List<WorkflowStep>();
|
|
if (has("dotnet")) testSteps.Add(StepAction("dotnet-test", "dotnet test", "dotnet-test"));
|
|
if (has("npm")) testSteps.Add(StepAction("npm-test", "npm test", "npm-test", scan.NodeWorkingDir));
|
|
if (has("python")) testSteps.Add(StepAction("python-pytest", "python -m pytest", "python-pytest"));
|
|
if (has("cargo")) testSteps.Add(StepAction("cargo-test", "cargo test", "cargo-test"));
|
|
if (testSteps.Count > 0)
|
|
{
|
|
yield return new WorkflowDefinition
|
|
{
|
|
Id = "test",
|
|
Label = "Run Tests",
|
|
Description = "Run detected test stacks",
|
|
Group = "Test",
|
|
Steps = testSteps
|
|
};
|
|
}
|
|
|
|
if (has("git"))
|
|
{
|
|
yield return new WorkflowDefinition
|
|
{
|
|
Id = "repo-health",
|
|
Label = "Repo Health",
|
|
Description = "Check repo status and fetch remotes",
|
|
Group = "Repo",
|
|
Steps =
|
|
[
|
|
StepAction("git-status", "git status", "git-status"),
|
|
StepAction("git-fetch", "git fetch", "git-fetch")
|
|
]
|
|
};
|
|
}
|
|
|
|
if (has("docker"))
|
|
{
|
|
yield return new WorkflowDefinition
|
|
{
|
|
Id = "containers",
|
|
Label = "Containers",
|
|
Description = scan.HasDockerCompose ? "Manage docker compose stack" : "Build docker image",
|
|
Group = "Containers",
|
|
Steps = scan.HasDockerCompose
|
|
? [StepAction("docker-compose-up", "docker compose up -d", "docker-compose-up")]
|
|
: [StepAction("docker-build", "docker build .", "docker-build")]
|
|
};
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<DebugProfileDefinition> BuildDebugProfiles(BootstrapScanResult scan)
|
|
{
|
|
var has = new Func<string, bool>(tool => scan.ToolFamilies.Contains(tool, StringComparer.OrdinalIgnoreCase));
|
|
if (has("dotnet"))
|
|
{
|
|
yield return new DebugProfileDefinition
|
|
{
|
|
Id = "dotnet-run",
|
|
Label = "Run .NET app",
|
|
Type = "dotnet",
|
|
Command = "dotnet",
|
|
Args = ["run"],
|
|
WorkingDir = ".",
|
|
Requires = [new ToolRequirement { Tool = "dotnet", InstallPolicy = InstallPolicy.Prompt }],
|
|
Attach = new DebugAttachConfig
|
|
{
|
|
Kind = "manual",
|
|
Note = "Attach your IDE debugger to the running dotnet process."
|
|
}
|
|
};
|
|
}
|
|
|
|
if (has("npm"))
|
|
{
|
|
yield return new DebugProfileDefinition
|
|
{
|
|
Id = "npm-dev",
|
|
Label = "Run npm dev server",
|
|
Type = "node",
|
|
Command = "npm",
|
|
Args = ["run", "dev"],
|
|
WorkingDir = string.IsNullOrWhiteSpace(scan.NodeWorkingDir) ? "." : scan.NodeWorkingDir!,
|
|
Requires =
|
|
[
|
|
new ToolRequirement { Tool = "node", InstallPolicy = InstallPolicy.Prompt },
|
|
new ToolRequirement { Tool = "npm", InstallPolicy = InstallPolicy.Prompt }
|
|
]
|
|
};
|
|
}
|
|
}
|
|
|
|
private static WorkflowStep StepAction(
|
|
string id,
|
|
string label,
|
|
string action,
|
|
string? workingDir = null,
|
|
IReadOnlyList<string>? actionArgs = null)
|
|
{
|
|
return new WorkflowStep
|
|
{
|
|
Id = id,
|
|
Label = label,
|
|
Action = action,
|
|
ActionArgs = actionArgs?.ToList() ?? [],
|
|
WorkingDir = string.IsNullOrWhiteSpace(workingDir) ? "." : workingDir
|
|
};
|
|
}
|
|
|
|
private static HashSet<string> DetectScriptHelpers(string projectRoot)
|
|
{
|
|
var scriptsDir = Path.Combine(projectRoot, "scripts");
|
|
if (!Directory.Exists(scriptsDir))
|
|
return new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
return Directory.EnumerateFiles(scriptsDir, "*.py", SearchOption.TopDirectoryOnly)
|
|
.Select(Path.GetFileName)
|
|
.Where(f => !string.IsNullOrWhiteSpace(f))
|
|
.Cast<string>()
|
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static IEnumerable<WorkflowDefinition> BuildScriptDrivenWorkflows(HashSet<string> scripts)
|
|
{
|
|
static WorkflowStep ScriptStep(string id, string label, params string[] scriptArgs) => new()
|
|
{
|
|
Id = id,
|
|
Label = label,
|
|
Command = "python",
|
|
Args = scriptArgs.ToList(),
|
|
WorkingDir = ".",
|
|
Requires = [new ToolRequirement { Tool = "python", InstallPolicy = InstallPolicy.Prompt }]
|
|
};
|
|
|
|
if (scripts.Contains("publish-sidecar.py"))
|
|
{
|
|
yield return new WorkflowDefinition
|
|
{
|
|
Id = "sidecar",
|
|
Label = "Publish Sidecar",
|
|
Description = "Publish sidecar service",
|
|
Group = "Build",
|
|
Steps = [ScriptStep("sidecar:run", "python scripts/publish-sidecar.py", "scripts/publish-sidecar.py")]
|
|
};
|
|
}
|
|
|
|
if (scripts.Contains("publish-app.py"))
|
|
{
|
|
yield return new WorkflowDefinition
|
|
{
|
|
Id = "web",
|
|
Label = "Build Web UI",
|
|
Description = "Build frontend assets",
|
|
Group = "Build",
|
|
Steps =
|
|
[
|
|
ScriptStep("web:run", "python scripts/publish-app.py --target web", "scripts/publish-app.py", "--target", "web")
|
|
]
|
|
};
|
|
|
|
yield return new WorkflowDefinition
|
|
{
|
|
Id = "tauri",
|
|
Label = "Build Tauri Desktop App",
|
|
Description = "Build desktop binary",
|
|
Group = "Build",
|
|
DependsOn = scripts.Contains("publish-sidecar.py") ? ["sidecar"] : [],
|
|
Steps =
|
|
[
|
|
ScriptStep("tauri:run", "python scripts/publish-app.py --target tauri --tauri-bundles none",
|
|
"scripts/publish-app.py", "--target", "tauri", "--tauri-bundles", "none")
|
|
]
|
|
};
|
|
}
|
|
|
|
if (scripts.Contains("publish-webgateway.py"))
|
|
{
|
|
yield return new WorkflowDefinition
|
|
{
|
|
Id = "webgateway",
|
|
Label = "Publish WebGateway",
|
|
Description = "Publish ASP.NET gateway",
|
|
Group = "Build",
|
|
DependsOn = scripts.Contains("publish-app.py") ? ["web"] : [],
|
|
Steps = [ScriptStep("webgateway:run", "python scripts/publish-webgateway.py", "scripts/publish-webgateway.py")]
|
|
};
|
|
}
|
|
|
|
if (scripts.Contains("sync-output.py"))
|
|
{
|
|
yield return new WorkflowDefinition
|
|
{
|
|
Id = "sync-output",
|
|
Label = "Sync Output",
|
|
Description = "Sync newest artifacts to output",
|
|
Group = "Build",
|
|
Steps = [ScriptStep("sync-output:run", "python scripts/sync-output.py", "scripts/sync-output.py")]
|
|
};
|
|
}
|
|
|
|
if (scripts.Contains("publish-output.py"))
|
|
{
|
|
yield return new WorkflowDefinition
|
|
{
|
|
Id = "stage-output",
|
|
Label = "Stage Output Bundle",
|
|
Description = "Publish and stage distributable output",
|
|
Group = "Build",
|
|
Steps = [ScriptStep("stage-output:run", "python scripts/publish-output.py", "scripts/publish-output.py")]
|
|
};
|
|
}
|
|
|
|
if (scripts.Contains("run-webgateway.py"))
|
|
{
|
|
yield return new WorkflowDefinition
|
|
{
|
|
Id = "run-gateway-dev",
|
|
Label = "Run WebGateway Server (Dev)",
|
|
Description = "Run gateway in development mode",
|
|
Group = "Dev",
|
|
Steps =
|
|
[
|
|
ScriptStep("run-gateway-dev:run", "python scripts/run-webgateway.py --mode Dev",
|
|
"scripts/run-webgateway.py", "--mode", "Dev")
|
|
]
|
|
};
|
|
}
|
|
}
|
|
|
|
private static string FindProjectRoot(string startDir)
|
|
{
|
|
var start = Path.GetFullPath(startDir);
|
|
var gitRoot = TryGetGitRoot(start);
|
|
if (!string.IsNullOrWhiteSpace(gitRoot))
|
|
return gitRoot!;
|
|
|
|
var best = start;
|
|
var bestScore = ScoreRoot(start);
|
|
var cursor = new DirectoryInfo(start);
|
|
while (cursor.Parent is not null)
|
|
{
|
|
cursor = cursor.Parent;
|
|
var score = ScoreRoot(cursor.FullName);
|
|
if (score > bestScore)
|
|
{
|
|
best = cursor.FullName;
|
|
bestScore = score;
|
|
}
|
|
}
|
|
|
|
return best;
|
|
}
|
|
|
|
private static int ScoreRoot(string path)
|
|
{
|
|
var score = 0;
|
|
if (File.Exists(Path.Combine(path, "package.json"))) score += 2;
|
|
if (File.Exists(Path.Combine(path, "pyproject.toml"))) score += 2;
|
|
if (File.Exists(Path.Combine(path, "Cargo.toml"))) score += 2;
|
|
if (File.Exists(Path.Combine(path, "docker-compose.yml")) ||
|
|
File.Exists(Path.Combine(path, "docker-compose.yaml")) ||
|
|
File.Exists(Path.Combine(path, "Dockerfile"))) score += 1;
|
|
if (Directory.EnumerateFiles(path, "*.sln", SearchOption.TopDirectoryOnly).Any()) score += 3;
|
|
if (Directory.Exists(Path.Combine(path, ".git"))) score += 1;
|
|
return score;
|
|
}
|
|
|
|
private static string? TryGetGitRoot(string start)
|
|
{
|
|
try
|
|
{
|
|
var psi = new System.Diagnostics.ProcessStartInfo
|
|
{
|
|
FileName = "git",
|
|
UseShellExecute = false,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
CreateNoWindow = true,
|
|
WorkingDirectory = start
|
|
};
|
|
psi.ArgumentList.Add("rev-parse");
|
|
psi.ArgumentList.Add("--show-toplevel");
|
|
using var process = System.Diagnostics.Process.Start(psi);
|
|
if (process is null)
|
|
return null;
|
|
var stdout = process.StandardOutput.ReadToEnd();
|
|
process.WaitForExit(2000);
|
|
if (process.ExitCode == 0 && !string.IsNullOrWhiteSpace(stdout))
|
|
return stdout.Trim();
|
|
return null;
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<string> EnumerateFilesBounded(string root, string pattern, int maxDepth)
|
|
{
|
|
var queue = new Queue<(string Dir, int Depth)>();
|
|
queue.Enqueue((root, 0));
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
var (dir, depth) = queue.Dequeue();
|
|
IEnumerable<string> files = [];
|
|
try
|
|
{
|
|
files = Directory.EnumerateFiles(dir, pattern, SearchOption.TopDirectoryOnly);
|
|
}
|
|
catch
|
|
{
|
|
// Ignore unreadable directories.
|
|
}
|
|
|
|
foreach (var file in files)
|
|
yield return file;
|
|
|
|
if (depth >= maxDepth)
|
|
continue;
|
|
|
|
IEnumerable<string> subdirs = [];
|
|
try
|
|
{
|
|
subdirs = Directory.EnumerateDirectories(dir, "*", SearchOption.TopDirectoryOnly);
|
|
}
|
|
catch
|
|
{
|
|
// Ignore unreadable directories.
|
|
}
|
|
|
|
foreach (var subdir in subdirs)
|
|
{
|
|
var name = Path.GetFileName(subdir);
|
|
if (ExcludedDirectories.Contains(name))
|
|
continue;
|
|
|
|
queue.Enqueue((subdir, depth + 1));
|
|
}
|
|
}
|
|
}
|
|
}
|