From 0699dcb2e412c3ee94760585f97f81d0cc1ce278 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 22 Jul 2026 09:44:18 +0200 Subject: [PATCH] Initial commit: OrbisForge CMake project scaffolder for OpenOrbis PS4 Toolchain --- .gitignore | 5 + .gitmodules | 3 + LICENSE | 21 ++ OrbisForge.slnx | 9 + README.md | 77 ++++++ external/UIFramework | 1 + src/OrbisForge.Cli/OrbisForge.Cli.csproj | 17 ++ src/OrbisForge.Cli/Program.cs | 131 ++++++++++ src/OrbisForge.Core/OrbisForge.Core.csproj | 11 + src/OrbisForge.Core/OrbisTargetType.cs | 42 ++++ src/OrbisForge.Core/Polyfills.cs | 11 + src/OrbisForge.Core/ProjectGenerator.cs | 105 ++++++++ src/OrbisForge.Core/ProjectOptions.cs | 38 +++ .../Templates/BuildScriptsTemplate.cs | 56 +++++ .../Templates/CMakeListsTemplate.cs | 53 ++++ .../Templates/CMakePresetsTemplate.cs | 26 ++ .../Templates/OrbisTargetsTemplate.cs | 214 ++++++++++++++++ .../Templates/ReadmeTemplate.cs | 47 ++++ .../Templates/SourceTemplate.cs | 44 ++++ .../Templates/ToolchainTemplate.cs | 43 ++++ src/OrbisForge.Gui/OrbisForge.Gui.csproj | 18 ++ src/OrbisForge.Gui/Program.cs | 16 ++ src/OrbisForge.Gui/WizardForm.cs | 230 ++++++++++++++++++ src/OrbisForge.Gui/WslEnvironment.cs | 121 +++++++++ 24 files changed, 1339 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 LICENSE create mode 100644 OrbisForge.slnx create mode 100644 README.md create mode 160000 external/UIFramework create mode 100644 src/OrbisForge.Cli/OrbisForge.Cli.csproj create mode 100644 src/OrbisForge.Cli/Program.cs create mode 100644 src/OrbisForge.Core/OrbisForge.Core.csproj create mode 100644 src/OrbisForge.Core/OrbisTargetType.cs create mode 100644 src/OrbisForge.Core/Polyfills.cs create mode 100644 src/OrbisForge.Core/ProjectGenerator.cs create mode 100644 src/OrbisForge.Core/ProjectOptions.cs create mode 100644 src/OrbisForge.Core/Templates/BuildScriptsTemplate.cs create mode 100644 src/OrbisForge.Core/Templates/CMakeListsTemplate.cs create mode 100644 src/OrbisForge.Core/Templates/CMakePresetsTemplate.cs create mode 100644 src/OrbisForge.Core/Templates/OrbisTargetsTemplate.cs create mode 100644 src/OrbisForge.Core/Templates/ReadmeTemplate.cs create mode 100644 src/OrbisForge.Core/Templates/SourceTemplate.cs create mode 100644 src/OrbisForge.Core/Templates/ToolchainTemplate.cs create mode 100644 src/OrbisForge.Gui/OrbisForge.Gui.csproj create mode 100644 src/OrbisForge.Gui/Program.cs create mode 100644 src/OrbisForge.Gui/WizardForm.cs create mode 100644 src/OrbisForge.Gui/WslEnvironment.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0e1f3ce --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +publish/ +.vs/ +*.user diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..9c27731 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "external/UIFramework"] + path = external/UIFramework + url = https://github.com/xeghosted/UIFramework.git diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..50c2629 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/OrbisForge.slnx b/OrbisForge.slnx new file mode 100644 index 0000000..2f9ec5c --- /dev/null +++ b/OrbisForge.slnx @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..d0252bf --- /dev/null +++ b/README.md @@ -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). diff --git a/external/UIFramework b/external/UIFramework new file mode 160000 index 0000000..c473097 --- /dev/null +++ b/external/UIFramework @@ -0,0 +1 @@ +Subproject commit c47309784f6dd98ab6098e2f60672f29c1c74a41 diff --git a/src/OrbisForge.Cli/OrbisForge.Cli.csproj b/src/OrbisForge.Cli/OrbisForge.Cli.csproj new file mode 100644 index 0000000..2bdeee1 --- /dev/null +++ b/src/OrbisForge.Cli/OrbisForge.Cli.csproj @@ -0,0 +1,17 @@ + + + + Exe + net8.0 + enable + enable + true + orbisforge + OrbisForge.Cli + + + + + + + diff --git a/src/OrbisForge.Cli/Program.cs b/src/OrbisForge.Cli/Program.cs new file mode 100644 index 0000000..b7dd55a --- /dev/null +++ b/src/OrbisForge.Cli/Program.cs @@ -0,0 +1,131 @@ +using OrbisForge.Core; + +var help = """ + OrbisForge - project scaffolder for the OpenOrbis PS4 Toolchain (CMake-based) + + Usage: + orbisforge new --name --type exe|prx|sprx [options] + + Options: + --name Project / target name (required) + --type exe, prx, or sprx (required) + --output Output directory (default: current directory) + --pkg EXE only: also scaffold a full .pkg installer step + --ptype Override create-fself -ptype (advanced) + --toolchain-path

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]; +} diff --git a/src/OrbisForge.Core/OrbisForge.Core.csproj b/src/OrbisForge.Core/OrbisForge.Core.csproj new file mode 100644 index 0000000..695bca7 --- /dev/null +++ b/src/OrbisForge.Core/OrbisForge.Core.csproj @@ -0,0 +1,11 @@ + + + + net8.0;netstandard2.0 + 12.0 + enable + enable + true + + + diff --git a/src/OrbisForge.Core/OrbisTargetType.cs b/src/OrbisForge.Core/OrbisTargetType.cs new file mode 100644 index 0000000..a5ac73b --- /dev/null +++ b/src/OrbisForge.Core/OrbisTargetType.cs @@ -0,0 +1,42 @@ +namespace OrbisForge.Core; + +///

+/// The PS4 artifact kind a generated project produces. +/// Maps to create-fself's --eboot vs --lib packaging mode and a default -ptype. +/// +public enum OrbisTargetType +{ + /// Executable homebrew app -> eboot.bin (ptype "fake"). + Exe, + + /// User-mode relocatable library -> <name>.prx (ptype "fake"). + Prx, + + /// Signed system library/module -> <name>.sprx (ptype "system_dynlib"). + 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; + } + } +} diff --git a/src/OrbisForge.Core/Polyfills.cs b/src/OrbisForge.Core/Polyfills.cs new file mode 100644 index 0000000..d492a0d --- /dev/null +++ b/src/OrbisForge.Core/Polyfills.cs @@ -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 diff --git a/src/OrbisForge.Core/ProjectGenerator.cs b/src/OrbisForge.Core/ProjectGenerator.cs new file mode 100644 index 0000000..d6ff2c9 --- /dev/null +++ b/src/OrbisForge.Core/ProjectGenerator.cs @@ -0,0 +1,105 @@ +using OrbisForge.Core.Templates; + +namespace OrbisForge.Core; + +public sealed class GeneratedProject +{ + public string RootDirectory { get; set; } = ""; + public IReadOnlyList FilesWritten { get; set; } = new List(); + public IReadOnlyList Warnings { get; set; } = new List(); +} + +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(); + var warnings = new List(); + + 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 files, List 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 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"); +} diff --git a/src/OrbisForge.Core/ProjectOptions.cs b/src/OrbisForge.Core/ProjectOptions.cs new file mode 100644 index 0000000..bdc00cf --- /dev/null +++ b/src/OrbisForge.Core/ProjectOptions.cs @@ -0,0 +1,38 @@ +namespace OrbisForge.Core; + +/// Everything needed to scaffold one OpenOrbis CMake project. +public sealed class ProjectOptions +{ + /// Project / target name. Used as the CMake target name and output file stem. + public string Name { get; set; } = ""; + + public OrbisTargetType Type { get; set; } + + /// Directory the project is written into (created if missing). + public string OutputDirectory { get; set; } = "."; + + /// EXE projects only: also scaffold the full retail-style .pkg packaging step + /// (param.sfo + create-gp4 + PkgTool.Core pkg_build), not just eboot.bin. + public bool IncludePackaging { get; set; } + + /// Override for create-fself's -ptype. Null uses the type's default + /// ("fake" for EXE/PRX, "system_dynlib" for SPRX). + public string? PType { get; set; } + + /// 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. + 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; + } +} diff --git a/src/OrbisForge.Core/Templates/BuildScriptsTemplate.cs b/src/OrbisForge.Core/Templates/BuildScriptsTemplate.cs new file mode 100644 index 0000000..3f14ec1 --- /dev/null +++ b/src/OrbisForge.Core/Templates/BuildScriptsTemplate.cs @@ -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" + """; +} diff --git a/src/OrbisForge.Core/Templates/CMakeListsTemplate.cs b/src/OrbisForge.Core/Templates/CMakeListsTemplate.cs new file mode 100644 index 0000000..43817c1 --- /dev/null +++ b/src/OrbisForge.Core/Templates/CMakeListsTemplate.cs @@ -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(); + } +} diff --git a/src/OrbisForge.Core/Templates/CMakePresetsTemplate.cs b/src/OrbisForge.Core/Templates/CMakePresetsTemplate.cs new file mode 100644 index 0000000..5ea808a --- /dev/null +++ b/src/OrbisForge.Core/Templates/CMakePresetsTemplate.cs @@ -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" + } + ] + } + """; +} diff --git a/src/OrbisForge.Core/Templates/OrbisTargetsTemplate.cs b/src/OrbisForge.Core/Templates/OrbisTargetsTemplate.cs new file mode 100644 index 0000000..464e972 --- /dev/null +++ b/src/OrbisForge.Core/Templates/OrbisTargetsTemplate.cs @@ -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( + # TYPE EXE|PRX|SPRX + # SOURCES + # [LIBS ] # default: c kernel c++ + # [PTYPE ] # default: "fake" (EXE/PRX) or "system_dynlib" (SPRX) + # [PAID ] # default: 0x3800000000000011 + # [LIBNAME ] # PRX/SPRX only, passed to create-fself -libname + # ) + # + # Links 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 -> .prx + # SPRX -> .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=$ + -out=$.oelf + ${_pkg_flag} "${_out_path}" + -ptype ${_ptype} + --paid ${ORBIS_PAID} + ${_libname_args} + COMMAND "${OO_PS4_TOOLCHAIN_BIN}/readoelf" "$.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( + # TITLE_ID CONTENT_ID TITLE [VERSION ] + # [ASSETS_DIR ] [SCE_MODULE_DIR ] + # ) + # + # Builds a full retail-style .pkg installer from an EXE target created by + # add_orbis_target(): param.sfo -> pkg.gp4 -> .pkg, exactly + # mirroring the toolchain's own sample Makefiles. Adds a "_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() + """; +} diff --git a/src/OrbisForge.Core/Templates/ReadmeTemplate.cs b/src/OrbisForge.Core/Templates/ReadmeTemplate.cs new file mode 100644 index 0000000..436247d --- /dev/null +++ b/src/OrbisForge.Core/Templates/ReadmeTemplate.cs @@ -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 '' && 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 + """; + } +} diff --git a/src/OrbisForge.Core/Templates/SourceTemplate.cs b/src/OrbisForge.Core/Templates/SourceTemplate.cs new file mode 100644 index 0000000..6ab4909 --- /dev/null +++ b/src/OrbisForge.Core/Templates/SourceTemplate.cs @@ -0,0 +1,44 @@ +namespace OrbisForge.Core.Templates; + +internal static class SourceTemplate +{ + public static string MainCpp(string projectName) => $$""" + #include + #include + + 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 + #include + #include + + #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 + + int {{projectName}}_hello(char *buf, size_t buf_size, int num); + """; +} diff --git a/src/OrbisForge.Core/Templates/ToolchainTemplate.cs b/src/OrbisForge.Core/Templates/ToolchainTemplate.cs new file mode 100644 index 0000000..159dbf5 --- /dev/null +++ b/src/OrbisForge.Core/Templates/ToolchainTemplate.cs @@ -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") + """; +} diff --git a/src/OrbisForge.Gui/OrbisForge.Gui.csproj b/src/OrbisForge.Gui/OrbisForge.Gui.csproj new file mode 100644 index 0000000..1357074 --- /dev/null +++ b/src/OrbisForge.Gui/OrbisForge.Gui.csproj @@ -0,0 +1,18 @@ + + + + WinExe + net48 + 7.3 + true + OrbisForge + OrbisForge.Gui + + + + + + + + + diff --git a/src/OrbisForge.Gui/Program.cs b/src/OrbisForge.Gui/Program.cs new file mode 100644 index 0000000..15b1f48 --- /dev/null +++ b/src/OrbisForge.Gui/Program.cs @@ -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()); + } + } +} diff --git a/src/OrbisForge.Gui/WizardForm.cs b/src/OrbisForge.Gui/WizardForm.cs new file mode 100644 index 0000000..0404446 --- /dev/null +++ b/src/OrbisForge.Gui/WizardForm.cs @@ -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); + } + } +} diff --git a/src/OrbisForge.Gui/WslEnvironment.cs b/src/OrbisForge.Gui/WslEnvironment.cs new file mode 100644 index 0000000..4d2d8d8 --- /dev/null +++ b/src/OrbisForge.Gui/WslEnvironment.cs @@ -0,0 +1,121 @@ +using System; +using System.Diagnostics; +using System.IO; + +namespace OrbisForge.Gui +{ + /// + /// 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. + /// + 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; + } + } + } +}