Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Content.ModuleManager/ModuleManifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public sealed class ModuleManifest
public string Version { get; set; } = string.Empty;
public List<ProjectInfo> Projects { get; set; } = new();

public bool Disabled { get; set; }

public string ManifestPath { get; set; } = string.Empty;

public string ModuleDirectory { get; set; } = string.Empty;
Expand Down
55 changes: 30 additions & 25 deletions Content.Packaging/ClientPackaging.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,21 +79,27 @@ private static List<string> GetClientModules(string path)

private static List<string> FindAllModules(string path = ".")
{
// Correct pathing to be in local folder if contentDir is empty.
if (string.IsNullOrEmpty(path))
path = ".";

var modules = new List<string> { "Content.Client", "Content.Shared", "Content.Shared.Database", "Content.ModuleManager" };

// Modules - Add modules from Modules/ directory
modules.AddRange(
ModuleDiscovery.DiscoverModules(path)
.Where(m => m.Type != ModuleRole.Server)
.Select(m => m.Name)
.Distinct()
);
var coreDepsPath = Path.Combine(path, "bin", "Content.Client", "Content.Client.deps.json");

foreach (var mod in ModuleDiscovery.DiscoverModules(path).Where(m => m.Type != ModuleRole.Server).DistinctBy(m => m.Name))
{
modules.Add(mod.Name);

return modules;
var moduleOutputDir = ModuleDiscovery.GetModuleOutputDir(mod.ProjectPath);
var moduleDepsPath = Path.Combine(moduleOutputDir, $"{mod.Name}.deps.json");

if (File.Exists(coreDepsPath) && File.Exists(moduleDepsPath))
{
modules.AddRange(DepsHandler.GetModuleUniqueAssemblies(coreDepsPath, moduleDepsPath));
}
}

return modules.Distinct().ToList();
}

public static async Task WriteResources(
Expand Down Expand Up @@ -168,18 +174,26 @@ private static Task WriteClientContentAssemblies(
{
var mainBinDir = Path.Combine(contentDir, "bin", "Content.Client");

var moduleAssemblyPaths = ModuleDiscovery.DiscoverModules(contentDir)
.Where(m => m.Type == ModuleRole.Client)
.ToDictionary(
m => m.Name,
m => Path.Combine(GetModuleRoot(m.ProjectPath), "bin", "Content.Client")
);
var moduleOutputDirs = new Dictionary<string, string>();

// Discover all non-Server modules
var allModules = ModuleDiscovery.DiscoverModules(contentDir)
.Where(m => m.Type != ModuleRole.Server)
.ToList();

// Map each module to its own build output directory
foreach (var module in allModules)
{
var moduleOutputDir = ModuleDiscovery.GetModuleOutputDir(module.ProjectPath);
moduleOutputDirs[module.Name] = moduleOutputDir;
}

foreach (var asm in contentAssemblies)
{
cancel.ThrowIfCancellationRequested();

var sourceDir = moduleAssemblyPaths.GetValueOrDefault(asm) ?? mainBinDir;
// Check module output dirs first, fall back to mainBinDir
var sourceDir = moduleOutputDirs.GetValueOrDefault(asm) ?? mainBinDir;

var dllPath = Path.Combine(sourceDir, $"{asm}.dll");
if (File.Exists(dllPath))
Expand All @@ -192,13 +206,4 @@ private static Task WriteClientContentAssemblies(

return Task.CompletedTask;
}

private static string GetModuleRoot(string projectPath)
{
// Extracts the module root from the project path
// e.g., "Modules/GoobStation/Content.Goobstation.Client/Content.Goobstation.Client.csproj"
// -> "Modules/GoobStation"
var projectDir = Path.GetDirectoryName(projectPath);
return Path.GetDirectoryName(projectDir)!;
}
}
30 changes: 29 additions & 1 deletion Content.Packaging/DepsHandler.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
using System.Text.Json;
// SPDX-FileCopyrightText: 2026 Space Station 14 Contributors
//
// SPDX-License-Identifier: MIT-WIZARDS

using System.Text.Json;
using System.Text.Json.Serialization;

namespace Content.Packaging;
Expand Down Expand Up @@ -57,6 +61,30 @@ private void RecursiveAddLibraries(string start, HashSet<string> set)
}
}

public static HashSet<string> GetModuleUniqueAssemblies(string coreDepsPath, string moduleDepsPath)
{
var core = Load(coreDepsPath);
var module = Load(moduleDepsPath);

var unique = new HashSet<string>(module.Libraries.Keys);
unique.ExceptWith(core.Libraries.Keys);

return unique;
}

public static IEnumerable<string> GetModuleUniqueDlls(string coreDepsPath, string moduleDepsPath)
{
var core = Load(coreDepsPath);
var module = Load(moduleDepsPath);

var uniqueLibs = new HashSet<string>(module.Libraries.Keys);
uniqueLibs.ExceptWith(core.Libraries.Keys);

return uniqueLibs
.Where(lib => module.Libraries.ContainsKey(lib))
.SelectMany(lib => module.Libraries[lib].GetDllNames());
}

public sealed class DepsData
{
[JsonInclude, JsonPropertyName("targets")]
Expand Down
12 changes: 12 additions & 0 deletions Content.Packaging/ModuleDiscovery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ public static class ModuleDiscovery
{
public record ModuleInfo(string Name, string ProjectPath, ModuleRole Type);

public static string GetModuleOutputDir(string projectPath)
{
var projectDir = Path.GetDirectoryName(projectPath)!;
var withTfm = Path.Combine(projectDir, "bin", "Debug", "net10.0");
if (Directory.Exists(withTfm))
return withTfm;
return Path.Combine(projectDir, "bin", "Debug");
}

/// <summary>
/// Discovers all modules by scanning for module.yml files in the Modules/ directory
/// </summary>
Expand All @@ -35,6 +44,9 @@ public static IEnumerable<ModuleInfo> DiscoverModules(string basePath = ".")
continue;
}

if (manifest.Disabled)
continue;

foreach (var project in manifest.Projects)
{
var projectPath = ModuleManifestLoader.GetProjectPath(project, manifest.ModuleDirectory);
Expand Down
1 change: 1 addition & 0 deletions Content.Packaging/ModuleManifestLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public static ModuleManifest LoadFromFile(string manifestPath)
Name = GetRequiredString(root, "name", manifestPath),
Id = GetRequiredString(root, "id", manifestPath),
Version = GetRequiredString(root, "version", manifestPath),
Disabled = root.TryGet("disabled", out var disabledNode) && disabledNode is ValueDataNode disabledVal && bool.TryParse(disabledVal.Value, out var disabled) && disabled,
};

if (!IsValidId(manifest.Id))
Expand Down
49 changes: 21 additions & 28 deletions Content.Packaging/ServerPackaging.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,6 @@ public static class ServerPackaging
.Select(o => o.Rid)
.ToList();

private static readonly List<string> CoreServerContentAssemblies = new()
{
"Content.Server.Database",
"Content.Server",
"Content.Shared",
"Content.Shared.Database",
"Content.ModuleManager", // I cant be fucked to figure out how to this dynamically
};

private static readonly List<string> ServerExtraAssemblies = new()
{
// Python script had Npgsql. though we want Npgsql.dll as well soooo
Expand Down Expand Up @@ -170,16 +161,27 @@ private static List<string> FindServerModules(string path = ".")

private static List<string> FindAllServerModules(string path = ".")
{
var modules = new List<string>(CoreServerContentAssemblies);
if (string.IsNullOrEmpty(path))
path = ".";

// Modules - Add modules from Modules/ directory
modules.AddRange(ModuleDiscovery.DiscoverModules(path)
.Where(m => m.Type != ModuleRole.Client)
.Select(m => m.Name)
.Distinct()
);
var modules = new List<string> { "Content.Server.Database", "Content.Server", "Content.Shared", "Content.Shared.Database", "Content.ModuleManager" };

var coreDepsPath = Path.Combine(path, "bin", "Content.Server", "Content.Server.deps.json");

foreach (var mod in ModuleDiscovery.DiscoverModules(path).Where(m => m.Type != ModuleRole.Client).DistinctBy(m => m.Name))
{
modules.Add(mod.Name);

var moduleOutputDir = ModuleDiscovery.GetModuleOutputDir(mod.ProjectPath);
var moduleDepsPath = Path.Combine(moduleOutputDir, $"{mod.Name}.deps.json");

return modules;
if (File.Exists(coreDepsPath) && File.Exists(moduleDepsPath))
{
modules.AddRange(DepsHandler.GetModuleUniqueAssemblies(coreDepsPath, moduleDepsPath));
}
}

return modules.Distinct().ToList();
}

private static async Task PublishClientServer(string runtime, string targetOs, string configuration)
Expand Down Expand Up @@ -272,10 +274,10 @@ private static Task WriteServerContentAssemblies(
var mainBinDir = Path.Combine(contentDir, "bin", "Content.Server");

var moduleAssemblyPaths = ModuleDiscovery.DiscoverModules(contentDir)
.Where(m => m.Type == ModuleRole.Server)
.Where(m => m.Type != ModuleRole.Client)
.ToDictionary(
m => m.Name,
m => Path.Combine(GetModuleRoot(m.ProjectPath), "bin", "Content.Server")
m => ModuleDiscovery.GetModuleOutputDir(m.ProjectPath)
);

foreach (var asm in contentAssemblies)
Expand All @@ -296,14 +298,5 @@ private static Task WriteServerContentAssemblies(
return Task.CompletedTask;
}

private static string GetModuleRoot(string projectPath)
{
// Extracts the module root from the project path
// e.g., "Modules/GoobStation/Content.Goobstation.Server/Content.Goobstation.Server.csproj"
// -> "Modules/GoobStation"
var projectDir = Path.GetDirectoryName(projectPath);
return Path.GetDirectoryName(projectDir)!;
}

private readonly record struct PlatformReg(string Rid, string TargetOs, bool BuildByDefault);
}
141 changes: 141 additions & 0 deletions Goobstation.Bootstrap/BootstrapBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// SPDX-FileCopyrightText: 2026 Space Station 14 Contributors
//
// SPDX-License-Identifier: MIT-WIZARDS

using System.Diagnostics;
using System.Runtime.InteropServices;
using Content.Packaging;
using Content.ModuleManager;

namespace Goobstation.Bootstrap;

public static class BootstrapBuilder
{
public static readonly string DotnetPath = FindDotnet();

private static string FindDotnet()
{
// Runtime dir is like /usr/share/dotnet/shared/Microsoft.NETCore.App/10.0.x/
var runtimeDir = RuntimeEnvironment.GetRuntimeDirectory();
var dotnetRoot = Path.GetFullPath(Path.Combine(runtimeDir, "..", "..", ".."));
var exe = Path.Combine(dotnetRoot, "dotnet");
if (File.Exists(exe))
return exe;
return "dotnet";
}

private static string FindRepoRoot()
{
var dir = AppContext.BaseDirectory;
while (dir != null)
{
if (File.Exists(Path.Combine(dir, "SpaceStation14.slnx")))
return dir;
dir = Path.GetDirectoryName(dir);
}
throw new Exception("Could not find repo root (SpaceStation14.slnx)");
}

public static Task BuildAll()
{
var repoRoot = FindRepoRoot();
Environment.CurrentDirectory = repoRoot;

var modules = ModuleDiscovery.DiscoverModules().ToList();

Console.WriteLine("Building core projects...");
RunDotnetBuild("Content.Client/Content.Client.csproj");
RunDotnetBuild("Content.Server/Content.Server.csproj");

foreach (var module in modules)
{
Console.WriteLine($"Building module {module.Name}...");
RunDotnetBuild(module.ProjectPath);
}

foreach (var module in modules)
{
Console.WriteLine($"Copying {module.Name} outputs...");

switch (module.Type)
{
case ModuleRole.Client:
CopyModuleOutputs(module, "bin/Content.Client");
break;
case ModuleRole.Server:
CopyModuleOutputs(module, "bin/Content.Server");
break;
case ModuleRole.Shared:
case ModuleRole.Common:
CopyModuleOutputs(module, "bin/Content.Client", "bin/Content.Server");
break;
}
}

Console.WriteLine("Build complete.");
return Task.CompletedTask;
}

private static void RunDotnetBuild(string projectPath)
{
var psi = new ProcessStartInfo
{
FileName = DotnetPath,
Arguments = $"build {projectPath} -c Debug --nologo /v:m /m",
UseShellExecute = false
};

using var process = Process.Start(psi);
if (process == null)
throw new Exception($"Failed to start dotnet build for {projectPath}");

process.WaitForExit();

if (process.ExitCode != 0)
throw new Exception($"Build failed for {projectPath} with exit code {process.ExitCode}");
}

private static void CopyModuleOutputs(ModuleDiscovery.ModuleInfo module, params string[] targetBinDirs)
{
var moduleOutputDir = ModuleDiscovery.GetModuleOutputDir(module.ProjectPath);

foreach (var targetBinDir in targetBinDirs)
{
var coreName = Path.GetFileName(targetBinDir);
var coreDepsPath = Path.Combine(targetBinDir, $"{coreName}.deps.json");
var moduleDepsPath = Path.Combine(moduleOutputDir, $"{module.Name}.deps.json");

var uniqueDlls = DepsHandler.GetModuleUniqueDlls(coreDepsPath, moduleDepsPath);

foreach (var dll in uniqueDlls)
{
var sourceDll = Path.Combine(moduleOutputDir, dll);
var targetDll = Path.Combine(targetBinDir, dll);

if (File.Exists(sourceDll))
File.Copy(sourceDll, targetDll, overwrite: true);

var sourcePdb = Path.ChangeExtension(sourceDll, ".pdb");
var targetPdb = Path.ChangeExtension(targetDll, ".pdb");

if (File.Exists(sourcePdb))
File.Copy(sourcePdb, targetPdb, overwrite: true);
}

var moduleAssembly = $"{module.Name}.dll";
var moduleAssemblyPdb = $"{module.Name}.pdb";

var sourceModuleDll = Path.Combine(moduleOutputDir, moduleAssembly);
var targetModuleDll = Path.Combine(targetBinDir, moduleAssembly);

if (File.Exists(sourceModuleDll))
File.Copy(sourceModuleDll, targetModuleDll, overwrite: true);

var sourceModulePdb = Path.Combine(moduleOutputDir, moduleAssemblyPdb);
var targetModulePdb = Path.Combine(targetBinDir, moduleAssemblyPdb);

if (File.Exists(sourceModulePdb))
File.Copy(sourceModulePdb, targetModulePdb, overwrite: true);
}
}
}
Loading
Loading