Initial commit: OrbisForge CMake project scaffolder for OpenOrbis PS4 Toolchain

This commit is contained in:
Claude Code committed 2026-07-22 09:44:18 +02:00
commit 0699dcb2e4
24 files changed
+1339

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
bin/
obj/
publish/
.vs/
*.user
+3
View File
@@ -0,0 +1,3 @@
[submodule "external/UIFramework"]
path = external/UIFramework
url = https://github.com/xeghosted/UIFramework.git
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 xeghosted
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+9
View File
@@ -0,0 +1,9 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/OrbisForge.Cli/OrbisForge.Cli.csproj" />
<Project Path="src/OrbisForge.Core/OrbisForge.Core.csproj" />
<Project Path="src/OrbisForge.Gui/OrbisForge.Gui.csproj" />
</Folder>
<Project Path="external/UIFramework/src/UIFramework.Controls/UIFramework.Controls.csproj" />
<Project Path="external/UIFramework/src/UIFramework.Core/UIFramework.Core.csproj" />
</Solution>
+77
View File
@@ -0,0 +1,77 @@
# OrbisForge
**Scaffold and build OpenOrbis PS4 homebrew projects with CMake — on Windows or Linux/WSL.**
OrbisForge generates self-contained CMake projects for the [OpenOrbis PS4
Toolchain](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain), producing `eboot.bin`
executables, `.prx` user libraries, `.sprx` signed system libraries, and (optionally) full
signed/encrypted `.pkg` installers — through a single `add_orbis_target()` CMake function.
A Windows GUI wizard and a cross-platform CLI both scaffold the same project layout.
> The GUI wizard is Windows-only, built on [UIFramework](https://github.com/xeghosted/UIFramework)
> (skinned WinForms). The CLI (`orbisforge`) runs natively on Windows, Linux, and WSL, published
> self-contained so no .NET install is required to use it.
## Features
- **All three artifact types** — EXE (`eboot.bin`), PRX, and SPRX, via one `add_orbis_target()`
call. `TYPE` selects the right CRT object (`crt1.o` vs `crtlib.o`), the right `create-fself`
packaging mode (`--eboot` vs `--lib`), and a sensible default `-ptype`.
- **Optional full `.pkg` installer** — `add_orbis_package()` stages `eboot.bin` and a generated
`param.sfo` into a directory tree matching the final package layout, then drives
`create-gp4` + `PkgTool.Core pkg_build` to produce a real signed/encrypted `.pkg`.
- **Cross-platform builds** — generated projects carry their own `cmake/` core (toolchain file +
target helpers), a `build.bat` that shells into WSL, and a `build.sh` for native Linux/WSL.
- **Automatic sanity check** — every packaged target runs `readoelf` over its own output as a
post-build step, catching malformed builds before you ever copy them to hardware.
- **GUI wizard (Windows)** — pick a name, type, output folder, and optional `.pkg` packaging;
detects your WSL distro and the OpenOrbis toolchain automatically.
- **CLI scaffolder (Windows + Linux + WSL)** — `orbisforge new --name foo --type sprx`.
## Requirements
- The [OpenOrbis PS4 Toolchain](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain) installed,
with `OO_PS4_TOOLCHAIN` set (in WSL, if building from Windows).
- clang, lld, and CMake ≥ 3.20 available wherever the actual build runs (WSL/Linux).
- Windows GUI: .NET Framework 4.8 (ships with Windows 10/11).
- CLI: nothing — the published binaries are self-contained.
## Using the CLI
```
orbisforge new --name my_plugin --type sprx --output E:\Projects\PS4
orbisforge new --name my_game --type exe --pkg
```
Run `orbisforge --help` for the full option list. Then, inside the generated project folder:
double-click `build.bat` on Windows, or run `bash build.sh` on Linux/WSL.
## Building from source
```
git clone --recurse-submodules https://github.com/xeghosted/OrbisForge.git
cd OrbisForge
dotnet build OrbisForge.slnx
```
`--recurse-submodules` is required — the GUI depends on
[UIFramework](https://github.com/xeghosted/UIFramework), pulled in as a git submodule at
`external/UIFramework`. If you already cloned without it: `git submodule update --init`.
To publish self-contained CLI binaries:
```
dotnet publish src/OrbisForge.Cli -c Release -r win-x64 --self-contained -p:PublishSingleFile=true -o publish/win-x64
dotnet publish src/OrbisForge.Cli -c Release -r linux-x64 --self-contained -p:PublishSingleFile=true -o publish/linux-x64
```
## Project layout
- `src/OrbisForge.Core` — scaffolding logic (multi-targeted `net8.0`/`netstandard2.0`)
- `src/OrbisForge.Cli` — cross-platform console scaffolder
- `src/OrbisForge.Gui` — Windows wizard (WinForms + UIFramework)
- `external/UIFramework` — git submodule
## License
MIT — see [LICENSE](LICENSE).
Vendored Submodule
+1
Submodule external/UIFramework added at c47309784f.
+17
View File
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
<AssemblyName>orbisforge</AssemblyName>
<RootNamespace>OrbisForge.Cli</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\OrbisForge.Core\OrbisForge.Core.csproj" />
</ItemGroup>
</Project>
+131
View File
@@ -0,0 +1,131 @@
using OrbisForge.Core;
var help = """
OrbisForge - project scaffolder for the OpenOrbis PS4 Toolchain (CMake-based)
Usage:
orbisforge new --name <name> --type exe|prx|sprx [options]
Options:
--name <name> Project / target name (required)
--type <type> exe, prx, or sprx (required)
--output <dir> Output directory (default: current directory)
--pkg EXE only: also scaffold a full .pkg installer step
--ptype <ptype> Override create-fself -ptype (advanced)
--toolchain-path <p> OO_PS4_TOOLCHAIN override, for locating packaging assets
when running on Windows (where that env var normally
only exists inside WSL). Point this at the toolchain's
\\wsl.localhost\... path if using --pkg from Windows.
-h, --help Show this help
Examples:
orbisforge new --name hello_cmake --type exe
orbisforge new --name my_plugin --type sprx --output E:\Projects\PS4
orbisforge new --name my_game --type exe --pkg
""";
if (args.Length == 0 || args[0] is "-h" or "--help" or "help")
{
Console.WriteLine(help);
return args.Length == 0 ? 1 : 0;
}
if (args[0] != "new")
{
Console.Error.WriteLine($"Unknown command '{args[0]}'. Run 'orbisforge --help'.");
return 1;
}
string? name = null;
string? typeText = null;
string outputDir = Directory.GetCurrentDirectory();
bool includePackaging = false;
string? ptype = null;
string? toolchainPath = null;
for (int i = 1; i < args.Length; i++)
{
switch (args[i])
{
case "--name":
name = RequireValue(args, ref i, "--name");
break;
case "--type":
typeText = RequireValue(args, ref i, "--type");
break;
case "--output":
outputDir = RequireValue(args, ref i, "--output");
break;
case "--pkg":
includePackaging = true;
break;
case "--ptype":
ptype = RequireValue(args, ref i, "--ptype");
break;
case "--toolchain-path":
toolchainPath = RequireValue(args, ref i, "--toolchain-path");
break;
case "-h":
case "--help":
Console.WriteLine(help);
return 0;
default:
Console.Error.WriteLine($"Unknown option '{args[i]}'.");
return 1;
}
}
if (string.IsNullOrWhiteSpace(name))
{
Console.Error.WriteLine("--name is required.");
return 1;
}
if (typeText is null || !OrbisTargetTypeExtensions.TryParse(typeText, out var type))
{
Console.Error.WriteLine("--type is required and must be one of: exe, prx, sprx.");
return 1;
}
if (includePackaging && type != OrbisTargetType.Exe)
{
Console.Error.WriteLine("--pkg is only valid with --type exe.");
return 1;
}
var options = new ProjectOptions
{
Name = name,
Type = type,
OutputDirectory = outputDir,
IncludePackaging = includePackaging,
PType = ptype,
ToolchainPathOverride = toolchainPath,
};
var result = ProjectGenerator.Generate(options);
Console.WriteLine($"Generated {options.Type.ToCMakeString()} project at: {result.RootDirectory}");
foreach (var file in result.FilesWritten)
{
Console.WriteLine($" {Path.GetRelativePath(result.RootDirectory, file)}");
}
foreach (var warning in result.Warnings)
{
Console.WriteLine($"warning: {warning}");
}
Console.WriteLine();
Console.WriteLine(OperatingSystem.IsWindows()
? "Next: double-click build.bat inside the project folder."
: "Next: run 'bash build.sh' inside the project folder.");
return 0;
static string RequireValue(string[] args, ref int i, string option)
{
if (i + 1 >= args.Length)
throw new ArgumentException($"{option} requires a value.");
return args[++i];
}
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;netstandard2.0</TargetFrameworks>
<LangVersion>12.0</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
</Project>
+42
View File
@@ -0,0 +1,42 @@
namespace OrbisForge.Core;
/// <summary>
/// The PS4 artifact kind a generated project produces.
/// Maps to create-fself's --eboot vs --lib packaging mode and a default -ptype.
/// </summary>
public enum OrbisTargetType
{
/// <summary>Executable homebrew app -> eboot.bin (ptype "fake").</summary>
Exe,
/// <summary>User-mode relocatable library -> &lt;name&gt;.prx (ptype "fake").</summary>
Prx,
/// <summary>Signed system library/module -> &lt;name&gt;.sprx (ptype "system_dynlib").</summary>
Sprx,
}
public static class OrbisTargetTypeExtensions
{
public static string ToCMakeString(this OrbisTargetType type) => type switch
{
OrbisTargetType.Exe => "EXE",
OrbisTargetType.Prx => "PRX",
OrbisTargetType.Sprx => "SPRX",
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null),
};
public static bool IsLibrary(this OrbisTargetType type) =>
type is OrbisTargetType.Prx or OrbisTargetType.Sprx;
public static bool TryParse(string text, out OrbisTargetType type)
{
switch (text.Trim().ToUpperInvariant())
{
case "EXE": case "BIN": type = OrbisTargetType.Exe; return true;
case "PRX": type = OrbisTargetType.Prx; return true;
case "SPRX": type = OrbisTargetType.Sprx; return true;
default: type = default; return false;
}
}
}
+11
View File
@@ -0,0 +1,11 @@
#if NETSTANDARD2_0
// `init` accessors need this marker type, which only ships in-box starting with
// net5.0. netstandard2.0 (used so this library can be referenced by both the
// modern .NET CLI and the net48 WinForms GUI) needs it supplied manually.
namespace System.Runtime.CompilerServices
{
internal static class IsExternalInit
{
}
}
#endif
+105
View File
@@ -0,0 +1,105 @@
using OrbisForge.Core.Templates;
namespace OrbisForge.Core;
public sealed class GeneratedProject
{
public string RootDirectory { get; set; } = "";
public IReadOnlyList<string> FilesWritten { get; set; } = new List<string>();
public IReadOnlyList<string> Warnings { get; set; } = new List<string>();
}
public static class ProjectGenerator
{
public static GeneratedProject Generate(ProjectOptions options)
{
var name = options.SanitizedName;
var root = Path.Combine(options.OutputDirectory, name);
var files = new List<string>();
var warnings = new List<string>();
Directory.CreateDirectory(root);
Directory.CreateDirectory(Path.Combine(root, "cmake"));
Directory.CreateDirectory(Path.Combine(root, "src"));
WriteFile(Path.Combine(root, "cmake", "oo-ps4-toolchain.cmake"), ToolchainTemplate.ToolchainCMake, files);
WriteFile(Path.Combine(root, "cmake", "OrbisTargets.cmake"), OrbisTargetsTemplate.OrbisTargetsCMake, files);
WriteFile(Path.Combine(root, "CMakeLists.txt"), CMakeListsTemplate.Render(options), files);
WriteFile(Path.Combine(root, "CMakePresets.json"), CMakePresetsTemplate.CMakePresetsJson, files);
WriteFile(Path.Combine(root, "README.md"), ReadmeTemplate.Render(options), files);
WriteFile(Path.Combine(root, "build.sh"), Unix(BuildScriptsTemplate.BuildSh), files);
WriteFile(Path.Combine(root, "build.bat"), Windows(BuildScriptsTemplate.BuildBat), files);
if (options.Type.IsLibrary())
{
WriteFile(Path.Combine(root, "src", $"{name}.cpp"), SourceTemplate.LibCpp(name), files);
WriteFile(Path.Combine(root, "src", $"{name}.h"), SourceTemplate.LibHeader(name), files);
}
else
{
WriteFile(Path.Combine(root, "src", "main.cpp"), SourceTemplate.MainCpp(options.Name), files);
}
if (options.IncludePackaging && options.Type == OrbisTargetType.Exe)
{
CopyPackagingAssets(root, options.ToolchainPathOverride, files, warnings);
}
return new GeneratedProject
{
RootDirectory = root,
FilesWritten = files,
Warnings = warnings,
};
}
private static void CopyPackagingAssets(string root, string? toolchainOverride, List<string> files, List<string> warnings)
{
var toolchain = toolchainOverride ?? Environment.GetEnvironmentVariable("OO_PS4_TOOLCHAIN");
if (string.IsNullOrEmpty(toolchain))
{
warnings.Add("OO_PS4_TOOLCHAIN is not set in this environment -- skipped copying sce_sys/sce_module " +
"packaging assets from the hello_world sample. Copy them manually before building the " +
"*_pkg target (see the toolchain's samples/hello_world directory).");
Directory.CreateDirectory(Path.Combine(root, "sce_sys", "about"));
Directory.CreateDirectory(Path.Combine(root, "sce_module"));
Directory.CreateDirectory(Path.Combine(root, "assets"));
return;
}
var sampleRoot = Path.Combine(toolchain, "samples", "hello_world");
var pairs = new (string From, string To)[]
{
(Path.Combine(sampleRoot, "sce_sys", "icon0.png"), Path.Combine(root, "sce_sys", "icon0.png")),
(Path.Combine(sampleRoot, "sce_sys", "about", "right.sprx"), Path.Combine(root, "sce_sys", "about", "right.sprx")),
(Path.Combine(sampleRoot, "sce_module", "libSceFios2.prx"), Path.Combine(root, "sce_module", "libSceFios2.prx")),
(Path.Combine(sampleRoot, "sce_module", "libc.prx"), Path.Combine(root, "sce_module", "libc.prx")),
};
foreach (var (from, to) in pairs)
{
if (!File.Exists(from))
{
warnings.Add($"Packaging asset not found, skipped: {from}");
continue;
}
Directory.CreateDirectory(Path.GetDirectoryName(to)!);
File.Copy(from, to, overwrite: true);
files.Add(to);
}
Directory.CreateDirectory(Path.Combine(root, "assets"));
}
private static void WriteFile(string path, string content, List<string> files)
{
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllText(path, content);
files.Add(path);
}
private static string Unix(string text) => text.Replace("\r\n", "\n");
private static string Windows(string text) => Unix(text).Replace("\n", "\r\n");
}
+38
View File
@@ -0,0 +1,38 @@
namespace OrbisForge.Core;
/// <summary>Everything needed to scaffold one OpenOrbis CMake project.</summary>
public sealed class ProjectOptions
{
/// <summary>Project / target name. Used as the CMake target name and output file stem.</summary>
public string Name { get; set; } = "";
public OrbisTargetType Type { get; set; }
/// <summary>Directory the project is written into (created if missing).</summary>
public string OutputDirectory { get; set; } = ".";
/// <summary>EXE projects only: also scaffold the full retail-style .pkg packaging step
/// (param.sfo + create-gp4 + PkgTool.Core pkg_build), not just eboot.bin.</summary>
public bool IncludePackaging { get; set; }
/// <summary>Override for create-fself's -ptype. Null uses the type's default
/// ("fake" for EXE/PRX, "system_dynlib" for SPRX).</summary>
public string? PType { get; set; }
/// <summary>Explicit path to the OpenOrbis toolchain root, used only for locating
/// packaging assets (icon0.png, sce_module stubs) to copy from samples/hello_world
/// when IncludePackaging is set. Falls back to the OO_PS4_TOOLCHAIN environment
/// variable when null -- callers running as a Windows process (where that variable
/// only exists inside WSL, not on the Windows side) should resolve and pass the
/// \\wsl.localhost\... UNC equivalent here instead.</summary>
public string? ToolchainPathOverride { get; set; }
public string SanitizedName => Sanitize(Name);
private static string Sanitize(string name)
{
var chars = name.Trim().Select(c => char.IsLetterOrDigit(c) || c is '_' or '-' ? c : '_').ToArray();
var result = new string(chars);
return result.Length == 0 ? "orbis_project" : result;
}
}
@@ -0,0 +1,56 @@
namespace OrbisForge.Core.Templates;
internal static class BuildScriptsTemplate
{
// Runs natively on Linux/WSL. If invoked from an interactive shell, .bashrc has
// already exported OO_PS4_TOOLCHAIN/PATH normally -- no special handling needed.
public const string BuildSh = """
#!/bin/bash
set -e
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -z "$OO_PS4_TOOLCHAIN" ]; then
echo "OO_PS4_TOOLCHAIN environment variable is not set. See the OpenOrbis PS4 Toolchain README." >&2
exit 1
fi
if [ "$1" = "clean" ]; then
rm -rf build
echo "Cleaned."
exit 0
fi
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=cmake/oo-ps4-toolchain.cmake
cmake --build build
""";
// Windows entry point: shells into WSL. Deliberately avoids ever referencing an
// unquoted $PATH inside the wsl bash -c command line -- WSL auto-injects the full
// Windows PATH (with spaces and parentheses like "Program Files (x86)") into that
// environment, and passing it through unquoted breaks bash's parser. Instead this
// queries OO_PS4_TOOLCHAIN once via its own isolated call and builds an explicit,
// clean PATH from it.
public const string BuildBat = """
@echo off
setlocal
set "SCRIPTDIR=%~dp0"
if "%SCRIPTDIR:~-1%"=="\" set "SCRIPTDIR=%SCRIPTDIR:~0,-1%"
for /f "usebackq tokens=2 delims==" %%i in (`wsl -- bash -ic "env | grep ^OO_PS4_TOOLCHAIN="`) do set "OO_TOOLCHAIN_WSL=%%i"
if "%OO_TOOLCHAIN_WSL%"=="" (
echo OO_PS4_TOOLCHAIN is not set in your WSL environment. See the OpenOrbis PS4 Toolchain README.
exit /b 1
)
for /f "usebackq delims=" %%i in (`wsl wslpath "%SCRIPTDIR%"`) do set "WSLDIR=%%i"
if "%~1"=="clean" (
wsl -- bash -c "rm -rf '%WSLDIR%/build'"
echo Cleaned.
goto :eof
)
wsl -- bash -c "export OO_PS4_TOOLCHAIN=%OO_TOOLCHAIN_WSL%; export PATH=%OO_TOOLCHAIN_WSL%/bin/linux:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1; cd '%WSLDIR%' && cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=cmake/oo-ps4-toolchain.cmake && cmake --build build"
""";
}
@@ -0,0 +1,53 @@
using System.Text;
namespace OrbisForge.Core.Templates;
internal static class CMakeListsTemplate
{
public static string Render(ProjectOptions options)
{
var name = options.SanitizedName;
var sb = new StringBuilder();
sb.AppendLine("cmake_minimum_required(VERSION 3.20)");
sb.AppendLine($"project({name} CXX)");
sb.AppendLine();
sb.AppendLine("if(NOT DEFINED ENV{OO_PS4_TOOLCHAIN})");
sb.AppendLine(" message(FATAL_ERROR \"OO_PS4_TOOLCHAIN environment variable is not set\")");
sb.AppendLine("endif()");
sb.AppendLine();
var sources = options.Type.IsLibrary()
? $"src/{name}.cpp"
: "src/main.cpp";
var ptypeArg = options.PType is null ? "" : $"\n PTYPE {options.PType}";
sb.AppendLine($"""
add_orbis_target({name}
TYPE {options.Type.ToCMakeString()}
SOURCES {sources}{ptypeArg}
)
""");
if (options.IncludePackaging && options.Type == OrbisTargetType.Exe)
{
var upper = name.ToUpperInvariant();
var titleId = ("OF" + upper).PadRight(9, '0').Substring(0, 9);
// Sony's content-ID shape: IV0000-<9-char title id>_00-<16-char suffix>.
var contentId = $"IV0000-{titleId}_00-{upper.PadRight(16, '0').Substring(0, 16)}";
sb.AppendLine();
sb.AppendLine($"""
add_orbis_package({name}
TITLE_ID {titleId}
CONTENT_ID {contentId}
TITLE "{options.Name}"
VERSION "1.00"
)
""");
}
return sb.ToString();
}
}
@@ -0,0 +1,26 @@
namespace OrbisForge.Core.Templates;
internal static class CMakePresetsTemplate
{
public const string CMakePresetsJson = """
{
"version": 6,
"configurePresets": [
{
"name": "default",
"displayName": "OpenOrbis (Linux/WSL native)",
"binaryDir": "${sourceDir}/build",
"cacheVariables": {
"CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/oo-ps4-toolchain.cmake"
}
}
],
"buildPresets": [
{
"name": "default",
"configurePreset": "default"
}
]
}
""";
}
@@ -0,0 +1,214 @@
namespace OrbisForge.Core.Templates;
internal static class OrbisTargetsTemplate
{
public const string OrbisTargetsCMake = """
# add_orbis_target() / add_orbis_package() - generated by OrbisForge.
#
# add_orbis_target(<name>
# TYPE EXE|PRX|SPRX
# SOURCES <files...>
# [LIBS <names...>] # default: c kernel c++
# [PTYPE <ptype>] # default: "fake" (EXE/PRX) or "system_dynlib" (SPRX)
# [PAID <hex>] # default: 0x3800000000000011
# [LIBNAME <name>] # PRX/SPRX only, passed to create-fself -libname
# )
#
# Links <name> as a PS4 ELF via the OpenOrbis toolchain, then runs
# create-fself as a POST_BUILD step to produce the final artifact:
# EXE -> eboot.bin
# PRX -> <name>.prx
# SPRX -> <name>.sprx
# readoelf then runs over the packaged .oelf as a build-time sanity check.
function(add_orbis_target name)
set(options "")
set(oneValueArgs TYPE PTYPE PAID LIBNAME)
set(multiValueArgs SOURCES LIBS)
cmake_parse_arguments(ORBIS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if(NOT ORBIS_SOURCES)
message(FATAL_ERROR "add_orbis_target(${name}): SOURCES is required")
endif()
if(NOT ORBIS_TYPE)
set(ORBIS_TYPE "EXE")
endif()
string(TOUPPER "${ORBIS_TYPE}" ORBIS_TYPE)
if(NOT ORBIS_LIBS)
set(ORBIS_LIBS c kernel c++)
endif()
add_executable(${name} ${ORBIS_SOURCES})
if(ORBIS_TYPE STREQUAL "EXE")
set(_crt_object "${OO_PS4_TOOLCHAIN}/lib/crt1.o")
set(_default_ptype "fake")
set(_out_name "eboot.bin")
set(_pkg_flag "--eboot")
elseif(ORBIS_TYPE STREQUAL "PRX")
set(_crt_object "${OO_PS4_TOOLCHAIN}/lib/crtlib.o")
set(_default_ptype "fake")
set(_out_name "${name}.prx")
set(_pkg_flag "--lib")
elseif(ORBIS_TYPE STREQUAL "SPRX")
set(_crt_object "${OO_PS4_TOOLCHAIN}/lib/crtlib.o")
set(_default_ptype "system_dynlib")
set(_out_name "${name}.sprx")
set(_pkg_flag "--lib")
else()
message(FATAL_ERROR "add_orbis_target(${name}): TYPE must be EXE, PRX, or SPRX (got '${ORBIS_TYPE}')")
endif()
# Order matters here and mirrors the toolchain's own sample Makefiles:
# object files, then -l libs, then the CRT object last, so crt1.o's own
# references into libkernel resolve against a linker that has already
# seen -lkernel.
target_link_libraries(${name} PRIVATE ${ORBIS_LIBS} "${_crt_object}")
if(ORBIS_PTYPE)
set(_ptype "${ORBIS_PTYPE}")
else()
set(_ptype "${_default_ptype}")
endif()
if(NOT ORBIS_PAID)
set(ORBIS_PAID "0x3800000000000011")
endif()
set(_libname_args "")
if(ORBIS_LIBNAME)
set(_libname_args -libname "${ORBIS_LIBNAME}")
endif()
set(_out_path "${CMAKE_CURRENT_BINARY_DIR}/${_out_name}")
add_custom_command(TARGET ${name} POST_BUILD
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/create-fself"
-in=$<TARGET_FILE:${name}>
-out=$<TARGET_FILE:${name}>.oelf
${_pkg_flag} "${_out_path}"
-ptype ${_ptype}
--paid ${ORBIS_PAID}
${_libname_args}
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/readoelf" "$<TARGET_FILE:${name}>.oelf"
BYPRODUCTS "${_out_path}"
COMMENT "orbis: packaging ${name} -> ${_out_name} (type=${ORBIS_TYPE}, ptype=${_ptype})"
VERBATIM
)
set_target_properties(${name} PROPERTIES
ORBIS_OUTPUT_PATH "${_out_path}"
ORBIS_OUTPUT_NAME "${_out_name}"
ORBIS_TYPE "${ORBIS_TYPE}"
)
endfunction()
# add_orbis_package(<target>
# TITLE_ID <id> CONTENT_ID <id> TITLE <string> [VERSION <string>]
# [ASSETS_DIR <dir>] [SCE_MODULE_DIR <dir>]
# )
#
# Builds a full retail-style .pkg installer from an EXE target created by
# add_orbis_target(): param.sfo -> pkg.gp4 -> <content-id>.pkg, exactly
# mirroring the toolchain's own sample Makefiles. Adds a "<target>_pkg"
# target you build explicitly (it is not part of the default `all`).
function(add_orbis_package target)
set(oneValueArgs TITLE_ID CONTENT_ID TITLE VERSION ASSETS_DIR SCE_MODULE_DIR)
cmake_parse_arguments(PKG "" "${oneValueArgs}" "" ${ARGN})
if(NOT PKG_TITLE_ID OR NOT PKG_CONTENT_ID OR NOT PKG_TITLE)
message(FATAL_ERROR "add_orbis_package(${target}): TITLE_ID, CONTENT_ID, and TITLE are required")
endif()
if(NOT PKG_VERSION)
set(PKG_VERSION "1.00")
endif()
if(NOT PKG_ASSETS_DIR)
set(PKG_ASSETS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/assets")
endif()
if(NOT PKG_SCE_MODULE_DIR)
set(PKG_SCE_MODULE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/sce_module")
endif()
get_target_property(_eboot ${target} ORBIS_OUTPUT_PATH)
if(NOT _eboot)
message(FATAL_ERROR "add_orbis_package(${target}): target has no ORBIS_OUTPUT_PATH; call add_orbis_target(${target} TYPE EXE ...) first")
endif()
set(_sce_sys "${CMAKE_CURRENT_SOURCE_DIR}/sce_sys")
# create-gp4's --files list has no separate source-vs-target-path syntax:
# each entry is a path relative to the working directory it runs from,
# and that SAME relative path becomes the file's location inside the
# package. eboot.bin and param.sfo are built elsewhere (under
# CMAKE_CURRENT_BINARY_DIR), so they need to be staged into a directory
# tree that already looks like the final package layout before create-gp4
# runs from it -- otherwise the package ends up with eboot.bin nested
# under a stray "build/" path instead of at its root, where the PS4
# loader requires it.
set(_stage "${CMAKE_CURRENT_BINARY_DIR}/pkg_root")
set(_sfo "${_stage}/sce_sys/param.sfo")
set(_gp4 "${_stage}/pkg.gp4")
# PkgTool.Core's pkg_build takes the gp4 path plus a "project directory"
# it resolves the gp4's relative file entries against AND writes the
# final .pkg into -- that has to be the same staging directory too.
set(_pkg_out "${_stage}/${PKG_CONTENT_ID}.pkg")
add_custom_command(
OUTPUT "${_sfo}"
COMMAND "${CMAKE_COMMAND}" -E make_directory "${_stage}/sce_sys/about" "${_stage}/sce_module" "${_stage}/assets"
COMMAND "${CMAKE_COMMAND}" -E copy "${_eboot}" "${_stage}/eboot.bin"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${_sce_sys}/icon0.png" "${_stage}/sce_sys/icon0.png"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${_sce_sys}/about/right.sprx" "${_stage}/sce_sys/about/right.sprx"
COMMAND "${CMAKE_COMMAND}" -E copy_directory "${PKG_SCE_MODULE_DIR}" "${_stage}/sce_module"
COMMAND "${CMAKE_COMMAND}" -E copy_directory "${PKG_ASSETS_DIR}" "${_stage}/assets"
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_new "${_sfo}"
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" APP_TYPE --type Integer --maxsize 4 --value 1
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" APP_VER --type Utf8 --maxsize 8 --value "${PKG_VERSION}"
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" ATTRIBUTE --type Integer --maxsize 4 --value 0
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" CATEGORY --type Utf8 --maxsize 4 --value gd
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" CONTENT_ID --type Utf8 --maxsize 48 --value "${PKG_CONTENT_ID}"
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" DOWNLOAD_DATA_SIZE --type Integer --maxsize 4 --value 0
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" SYSTEM_VER --type Integer --maxsize 4 --value 0
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" TITLE --type Utf8 --maxsize 128 --value "${PKG_TITLE}"
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" TITLE_ID --type Utf8 --maxsize 12 --value "${PKG_TITLE_ID}"
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" sfo_setentry "${_sfo}" VERSION --type Utf8 --maxsize 8 --value "${PKG_VERSION}"
DEPENDS "${target}"
COMMENT "orbis: staging package contents + generating param.sfo for ${target}"
VERBATIM
)
file(GLOB_RECURSE _assets RELATIVE "${PKG_ASSETS_DIR}" CONFIGURE_DEPENDS "${PKG_ASSETS_DIR}/*")
file(GLOB _modules RELATIVE "${PKG_SCE_MODULE_DIR}" CONFIGURE_DEPENDS "${PKG_SCE_MODULE_DIR}/*")
list(TRANSFORM _assets PREPEND "assets/")
list(TRANSFORM _modules PREPEND "sce_module/")
set(_all_files eboot.bin sce_sys/about/right.sprx sce_sys/param.sfo sce_sys/icon0.png ${_modules} ${_assets})
list(JOIN _all_files " " _files_arg)
add_custom_command(
OUTPUT "${_gp4}"
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/create-gp4"
-out "${_gp4}"
--content-id=${PKG_CONTENT_ID}
--files "${_files_arg}"
WORKING_DIRECTORY "${_stage}"
DEPENDS "${_sfo}"
COMMENT "orbis: generating pkg.gp4 for ${target}"
VERBATIM
)
add_custom_command(
OUTPUT "${_pkg_out}"
COMMAND "${OO_PS4_TOOLCHAIN_BIN}/PkgTool.Core" pkg_build "${_gp4}" "${_stage}"
WORKING_DIRECTORY "${_stage}"
DEPENDS "${_gp4}"
COMMENT "orbis: building ${PKG_CONTENT_ID}.pkg"
VERBATIM
)
add_custom_target(${target}_pkg DEPENDS "${_pkg_out}")
set_target_properties(${target}_pkg PROPERTIES ORBIS_PKG_PATH "${_pkg_out}")
endfunction()
""";
}
@@ -0,0 +1,47 @@
namespace OrbisForge.Core.Templates;
internal static class ReadmeTemplate
{
public static string Render(ProjectOptions options)
{
var name = options.SanitizedName;
var outputs = options.Type switch
{
OrbisTargetType.Exe => "build/eboot.bin",
OrbisTargetType.Prx => $"build/{name}.prx",
OrbisTargetType.Sprx => $"build/{name}.sprx",
_ => "build/",
};
var pkgNote = options.IncludePackaging && options.Type == OrbisTargetType.Exe
? "\nTo build the full retail-style installer: `cmake --build build --target {0}_pkg` (Windows: `wsl -- bash -c \"cd '<wsl-path>' && cmake --build build --target {0}_pkg\"`). Produces `build/pkg/*.pkg`.\n"
: "";
pkgNote = string.Format(pkgNote, name);
return $"""
# {options.Name}
Generated by OrbisForge for the [OpenOrbis PS4 Toolchain](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain).
Type: **{options.Type.ToCMakeString()}** -> `{outputs}`
## Prerequisites
- `OO_PS4_TOOLCHAIN` environment variable set (in WSL, if building from Windows)
- clang + lld + cmake available on the build machine (WSL/Linux)
## Building
**Windows:** double-click `build.bat`, or `build.bat clean` to wipe the build directory.
**Linux / WSL (native):** `bash build.sh`, or `bash build.sh clean` (use `bash` rather than `./build.sh` since files written from Windows don't carry the executable bit).
{pkgNote}
## Project layout
- `src/` - source files
- `cmake/oo-ps4-toolchain.cmake` - CMake toolchain file for the OpenOrbis clang/lld setup
- `cmake/OrbisTargets.cmake` - `add_orbis_target()` / `add_orbis_package()` helpers
- `CMakeLists.txt` - project definition
""";
}
}
@@ -0,0 +1,44 @@
namespace OrbisForge.Core.Templates;
internal static class SourceTemplate
{
public static string MainCpp(string projectName) => $$"""
#include <stdio.h>
#include <orbis/libkernel.h>
int main(void)
{
setvbuf(stdout, NULL, _IONBF, 0);
printf("Hello from {{projectName}}!\n");
sceKernelUsleep(2 * 1000000);
return 0;
}
""";
public static string LibCpp(string projectName) => $$"""
#include <stdio.h>
#include <string.h>
#include <sstream>
#include "{{projectName}}.h"
int {{projectName}}_hello(char *buf, size_t buf_size, int num)
{
std::ostringstream os;
os << "Hi from {{projectName}}! You passed: " << num;
std::string s = os.str();
strncpy(buf, s.c_str(), buf_size);
return 0;
}
""";
public static string LibHeader(string projectName) => $$"""
#pragma once
#include <stddef.h>
int {{projectName}}_hello(char *buf, size_t buf_size, int num);
""";
}
@@ -0,0 +1,43 @@
namespace OrbisForge.Core.Templates;
internal static class ToolchainTemplate
{
public const string ToolchainCMake = """
# CMake toolchain file for the OpenOrbis PS4 Toolchain.
# Generated by OrbisForge. Requires the OO_PS4_TOOLCHAIN environment variable.
#
# Usage: cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=cmake/oo-ps4-toolchain.cmake
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
if(NOT DEFINED ENV{OO_PS4_TOOLCHAIN})
message(FATAL_ERROR "OO_PS4_TOOLCHAIN environment variable is not set")
endif()
set(OO_PS4_TOOLCHAIN "$ENV{OO_PS4_TOOLCHAIN}" CACHE PATH "OpenOrbis PS4 toolchain root")
set(OO_PS4_TOOLCHAIN_BIN "${OO_PS4_TOOLCHAIN}/bin/linux" CACHE PATH "OpenOrbis PS4 toolchain host tools")
set(CMAKE_C_COMPILER clang)
set(CMAKE_CXX_COMPILER clang++)
set(CMAKE_C_COMPILER_TARGET x86_64-pc-freebsd12-elf)
set(CMAKE_CXX_COMPILER_TARGET x86_64-pc-freebsd12-elf)
# CMake's compiler sanity check tries to link a full executable, which fails
# against this target unless we skip straight to a static-library try-compile.
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
set(_oo_common_flags "-fPIC -funwind-tables -isysroot ${OO_PS4_TOOLCHAIN} -isystem ${OO_PS4_TOOLCHAIN}/include")
set(CMAKE_C_FLAGS_INIT "${_oo_common_flags}")
set(CMAKE_CXX_FLAGS_INIT "${_oo_common_flags} -isystem ${OO_PS4_TOOLCHAIN}/include/c++/v1")
# Base linker setup shared by every target. The CRT start object and the
# -l libs are added per-target by add_orbis_target() below, since
# executables and libraries need different CRT objects (crt1.o vs crtlib.o).
set(CMAKE_EXE_LINKER_FLAGS_INIT
"-fuse-ld=lld -nostdlib -nostartfiles -pie -Wl,--script=${OO_PS4_TOOLCHAIN}/link.x -Wl,--eh-frame-hdr -L${OO_PS4_TOOLCHAIN}/lib"
)
include("${CMAKE_CURRENT_LIST_DIR}/OrbisTargets.cmake")
""";
}
+18
View File
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net48</TargetFramework>
<LangVersion>7.3</LangVersion>
<UseWindowsForms>true</UseWindowsForms>
<AssemblyName>OrbisForge</AssemblyName>
<RootNamespace>OrbisForge.Gui</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\OrbisForge.Core\OrbisForge.Core.csproj" />
<ProjectReference Include="..\..\external\UIFramework\src\UIFramework.Core\UIFramework.Core.csproj" />
<ProjectReference Include="..\..\external\UIFramework\src\UIFramework.Controls\UIFramework.Controls.csproj" />
</ItemGroup>
</Project>
+16
View File
@@ -0,0 +1,16 @@
using System;
using System.Windows.Forms;
namespace OrbisForge.Gui
{
internal static class Program
{
[STAThread]
private static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new WizardForm());
}
}
}
+230
View File
@@ -0,0 +1,230 @@
using System;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using UIFramework.Controls;
using UIFramework.Core.Controls;
using OrbisForge.Core;
namespace OrbisForge.Gui
{
internal sealed class WizardForm : SkinnedForm
{
private readonly SkinTextBox _nameBox = new SkinTextBox();
private readonly SkinComboBox _typeBox = new SkinComboBox();
private readonly SkinTextBox _outputBox = new SkinTextBox();
private readonly CheckBox _pkgCheck = new CheckBox();
private readonly SkinLabel _envLabel = new SkinLabel();
private readonly SkinButton _generateButton = new SkinButton();
private readonly TextBox _statusBox = new TextBox();
private WslDetectionResult _wsl;
public WizardForm()
{
Text = "OrbisForge — OpenOrbis PS4 Project Wizard";
ClientSize = new Size(520, 460);
StartPosition = FormStartPosition.CenterScreen;
AutoScaleMode = AutoScaleMode.Dpi;
AutoScaleDimensions = new SizeF(96F, 96F);
var root = new SkinPanel { Dock = DockStyle.Fill };
Controls.Add(root);
const int left = 16;
const int fieldWidth = 320;
int y = 16;
var nameLabel = new SkinLabel { Text = "Project name:", Location = new Point(left, y), AutoSize = true };
y += 20;
_nameBox.Location = new Point(left, y);
_nameBox.Size = new Size(fieldWidth, 24);
_nameBox.PlaceholderText = "my_project";
y += 36;
var typeLabel = new SkinLabel { Text = "Type:", Location = new Point(left, y), AutoSize = true };
y += 20;
_typeBox.Items.Add("EXE (eboot.bin, homebrew app)");
_typeBox.Items.Add("PRX (user-mode library)");
_typeBox.Items.Add("SPRX (signed system library)");
_typeBox.SelectedIndex = 0;
_typeBox.Location = new Point(left, y);
_typeBox.Size = new Size(fieldWidth, 24);
_typeBox.SelectedIndexChanged += (s, e) => UpdatePkgCheckEnabled();
y += 36;
var outputLabel = new SkinLabel { Text = "Output folder:", Location = new Point(left, y), AutoSize = true };
y += 20;
_outputBox.Location = new Point(left, y);
_outputBox.Size = new Size(fieldWidth - 90, 24);
_outputBox.Text = Directory.GetCurrentDirectory();
var browseButton = new SkinButton { Text = "Browse…", AutoSize = true, Location = new Point(_outputBox.Right + 8, y) };
browseButton.Click += (s, e) => BrowseForFolder();
y += 36;
_pkgCheck.Text = "Also generate full .pkg installer (EXE only)";
_pkgCheck.Location = new Point(left, y);
_pkgCheck.AutoSize = true;
y += 28;
_envLabel.Location = new Point(left, y);
_envLabel.AutoSize = true;
_envLabel.Text = "Checking WSL / OpenOrbis toolchain…";
y += 28;
_generateButton.Text = "Generate Project";
_generateButton.AutoSize = true;
_generateButton.Location = new Point(left, y);
_generateButton.Click += (s, e) => Generate();
y += 44;
_statusBox.Multiline = true;
_statusBox.ReadOnly = true;
_statusBox.ScrollBars = ScrollBars.Vertical;
_statusBox.Location = new Point(left, y);
_statusBox.Size = new Size(480, 160);
_statusBox.BorderStyle = BorderStyle.FixedSingle;
root.Controls.Add(nameLabel);
root.Controls.Add(_nameBox);
root.Controls.Add(typeLabel);
root.Controls.Add(_typeBox);
root.Controls.Add(outputLabel);
root.Controls.Add(_outputBox);
root.Controls.Add(browseButton);
root.Controls.Add(_pkgCheck);
root.Controls.Add(_envLabel);
root.Controls.Add(_generateButton);
root.Controls.Add(_statusBox);
UpdatePkgCheckEnabled();
Shown += (s, e) => DetectEnvironment();
}
private void UpdatePkgCheckEnabled()
{
bool isExe = SelectedType() == OrbisTargetType.Exe;
_pkgCheck.Enabled = isExe;
if (!isExe) _pkgCheck.Checked = false;
}
private OrbisTargetType SelectedType()
{
switch (_typeBox.SelectedIndex)
{
case 1: return OrbisTargetType.Prx;
case 2: return OrbisTargetType.Sprx;
default: return OrbisTargetType.Exe;
}
}
private void DetectEnvironment()
{
_envLabel.Text = "Checking WSL / OpenOrbis toolchain…";
Cursor = Cursors.WaitCursor;
try
{
_wsl = WslEnvironment.Detect();
}
finally
{
Cursor = Cursors.Default;
}
if (!_wsl.WslAvailable)
{
_envLabel.Text = "WSL not detected — install it before building generated projects.";
}
else if (_wsl.ToolchainFound)
{
_envLabel.Text = "Found: " + _wsl.DistroName + " — " + _wsl.ToolchainWslPath;
}
else
{
_envLabel.Text = "WSL found (" + _wsl.DistroName + "), but OO_PS4_TOOLCHAIN is not set there.";
}
}
private void BrowseForFolder()
{
using (var dialog = new FolderBrowserDialog())
{
dialog.SelectedPath = _outputBox.Text;
if (dialog.ShowDialog(this) == DialogResult.OK)
{
_outputBox.Text = dialog.SelectedPath;
}
}
}
private void Generate()
{
string name = _nameBox.Text.Trim();
if (name.Length == 0)
{
MessageBox.Show(this, "Enter a project name first.", "OrbisForge", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
var type = SelectedType();
bool includePkg = _pkgCheck.Checked && type == OrbisTargetType.Exe;
string toolchainOverride = null;
if (includePkg && _wsl != null && _wsl.ToolchainFound)
{
toolchainOverride = _wsl.ToolchainUncPath;
}
var options = new ProjectOptions
{
Name = name,
Type = type,
OutputDirectory = _outputBox.Text,
IncludePackaging = includePkg,
ToolchainPathOverride = toolchainOverride,
};
try
{
var result = ProjectGenerator.Generate(options);
_statusBox.Text = "Generated " + type + " project at:\r\n" + result.RootDirectory + "\r\n\r\nFiles:\r\n";
foreach (var file in result.FilesWritten)
{
_statusBox.AppendText(" " + MakeRelative(result.RootDirectory, file) + "\r\n");
}
foreach (var warning in result.Warnings)
{
_statusBox.AppendText("\r\nwarning: " + warning + "\r\n");
}
_statusBox.AppendText("\r\nNext: double-click build.bat inside the project folder.\r\n");
var openResult = MessageBox.Show(
this,
"Project generated at:\n" + result.RootDirectory + "\n\nOpen the folder now?",
"OrbisForge",
MessageBoxButtons.YesNo,
MessageBoxIcon.Information);
if (openResult == DialogResult.Yes)
{
Process.Start("explorer.exe", "\"" + result.RootDirectory + "\"");
}
}
catch (Exception ex)
{
_statusBox.Text = "Error: " + ex.Message;
MessageBox.Show(this, ex.Message, "OrbisForge — generation failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private static string MakeRelative(string root, string fullPath)
{
var rootUri = new Uri(root.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar);
var fileUri = new Uri(fullPath);
return Uri.UnescapeDataString(rootUri.MakeRelativeUri(fileUri).ToString()).Replace('/', Path.DirectorySeparatorChar);
}
}
}
+121
View File
@@ -0,0 +1,121 @@
using System;
using System.Diagnostics;
using System.IO;
namespace OrbisForge.Gui
{
/// <summary>
/// Detects the OpenOrbis toolchain inside WSL from a Windows process, by shelling
/// out to wsl.exe (same approach the generated build.bat scripts use) and then
/// resolving the WSL-side path to its \\wsl.localhost\... UNC equivalent so a
/// Windows process can read files from it directly.
/// </summary>
internal sealed class WslDetectionResult
{
public bool WslAvailable;
public string DistroName;
public string ToolchainWslPath;
public string ToolchainUncPath;
public bool ToolchainFound
{
get { return !string.IsNullOrEmpty(ToolchainUncPath) && Directory.Exists(ToolchainUncPath); }
}
}
internal static class WslEnvironment
{
public static WslDetectionResult Detect()
{
var result = new WslDetectionResult();
string distroList = RunProcess("wsl.exe", "-l -q", 4000);
if (distroList == null)
{
return result;
}
result.WslAvailable = true;
string distro = FirstNonEmptyLine(distroList);
if (distro == null)
{
return result;
}
result.DistroName = distro;
// Deliberately avoids direct "$VAR" expansion inside the quoted wsl.exe
// command line -- across the cmd/wsl/bash quoting layers that reliably
// comes back empty even though the variable is genuinely set (confirmed
// via "env | grep", which does not need nested quoting and works).
string envLine = RunProcess("wsl.exe", "-d " + distro + " -- bash -ic \"env | grep ^OO_PS4_TOOLCHAIN=\"", 6000);
if (string.IsNullOrEmpty(envLine))
{
return result;
}
int eq = envLine.IndexOf('=');
if (eq < 0)
{
return result;
}
string toolchainPath = envLine.Substring(eq + 1).Trim();
if (toolchainPath.Length == 0)
{
return result;
}
result.ToolchainWslPath = toolchainPath;
// /home/bbc/OpenOrbis-PS4-Toolchain -> \\wsl.localhost\Ubuntu\home\bbc\OpenOrbis-PS4-Toolchain
string windowsRelative = toolchainPath.Replace('/', '\\');
if (windowsRelative.StartsWith("\\"))
{
windowsRelative = windowsRelative.Substring(1);
}
result.ToolchainUncPath = "\\\\wsl.localhost\\" + distro + "\\" + windowsRelative;
return result;
}
private static string FirstNonEmptyLine(string text)
{
var lines = text.Replace("\r", "").Split('\n');
foreach (var line in lines)
{
var trimmed = line.Trim();
if (trimmed.Length > 0)
{
return trimmed;
}
}
return null;
}
private static string RunProcess(string fileName, string arguments, int timeoutMs)
{
try
{
var psi = new ProcessStartInfo(fileName, arguments)
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
using (var process = Process.Start(psi))
{
string output = process.StandardOutput.ReadToEnd();
if (!process.WaitForExit(timeoutMs))
{
try { process.Kill(); } catch { }
return null;
}
return process.ExitCode == 0 ? output : null;
}
}
catch
{
return null;
}
}
}
}