diff --git a/README.md b/README.md index be7b73b1c..6abf72040 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,7 @@ If you are planning on doing mod development using UE4SS, you can do the same as - Visual Studio 2019 (recent versions), and Visual Studio 2022 will work. - More compilers will hopefully be supported in the future. - Rust toolchain 1.73.0 or greater -- [xmake](https://xmake.io/#/) - +- [xmake >= 2.9.2](https://xmake.io/#/) ## Build instructions @@ -68,9 +67,18 @@ If you are planning on doing mod development using UE4SS, you can do the same as There are several different ways you can build UE4SS. -### Building from cli +## Building from cli + +### Configuration settings + +`xmake` allows you to flexibly configure some build options to suit your specific needs. The following is a non-comprehensive list of configuration settings you might find useful. + +> [!IMPORTANT] +> All configuration changes are made by using the `xmake config` command. You can also use `xmake f` as an alias for con**f**ig. + +After configuring `xmake` with any of the following options, you can build the project with `xmake` or `xmake build`. -Configure the project using this command: `xmake f -m ""` +#### Mode The build modes are structured as follows: `____` @@ -89,26 +97,70 @@ Currently supported options for these are: * `Platform` * `Win64` - 64-bit windows +> [!TIP] +> Configure the project using this command: `xmake f -m ""`. `-m` is an alias for --**m**ode=\. + +#### Patternsleuth (Experimental) + +By default, the patternsleuth tool installs itself as an xmake package. If you do not intend on modifying the patternsleuth source code, then you don't have to configure anything special. If you want to be able to modify the patternsleuth source code, you have to supply the `--patternsleuth=local` option to `xmake config` in order to recompile patternsleuth as part of the UE4SS build. + +#### Proxy Path + +By default, UE4SS generates a proxy based on `C:\Windows\System32\dwmapi.dll`. If you want to change this for any reason, you can supply the `--ue4ssProxyPath=` to the `xmake config` command.. + +#### Profiler Flavor -Now to build it, just run `xmake` +By default, UE4SS uses Tracy for profiling. You can pass `--profilerFlavor=` to the `xmake config` command to set the profiler flavor. The currently supported flavors are `Tracy`, `Superluminal`, and `None`. + +### Helpful `xmake` commands + +You may encounter use for the some of the more advanced `xmake` commands. A non-comprehensive list of some useful commands is included below. + +| Syntax | Aliases | Uses | +| --- | --- | --- | +| `xmake --yes` | `xmake -y` | Automatically confirm any user prompts. | +| `xmake --verbose ` | `xmake -v ` | Enable verbose level logging. | +| `xmake --Diagnostic ` | `xmake -D ` | Enable diagnostic level logging. | +| `xmake --verbose --Diagnostic --yes ` | `xmake -vDy ` | You can combine most flags into a single `-flagCombo`. | +| `xmake config` | `xmake f` | Configure xmake with any of [these options](#configuration-settings). | +| `xmake clean --all` | `xmake c --all` | Cleans binaries and intermediate output of all targets. | +| `xmake clean ` | `xmake c ` | Cleans binaries and intermediates of a specific target. | +| `xmake build` | `xmake b` | Incrementally builds UE4SS using input file detection. | +| `xmake build --rebuild` | `xmake b -r` | Forces a full rebuild of UE4SS. | +| `xmake build ` | `xmake b ` | Incrementally builds a specific target. | +| `xmake show` | | Shows xmake info and current project info. | +| `xmake show --target=` | `xmake show -t ` | Prints lots of information about a target. Useful for debugging scripts, compiler flags, dependency tree, etc. | +| `xmake require --clean` | `xmake q -c` | Clears all package caches and uninstalls all not-referenced packages. | +| `xmake require --force` | `xmake q -f` | Force re-installs all dependency packages. | +| `xmake require --list` | `xmake q -l` | Lists all packages that are needed for the project. | +| `xmake project --kind=vsxmake2022 --modes="Game__Shipping__Win64"` | `xmake project -k vsxmake2022 -m "Game__Shipping__Win64"` | Generates a [Visual Studio project](#visual-studio--rider) based on your current `xmake config`uration. You can specify multiple modes to generate by supplying `-m "Comma,Separated,Modes"`. If you do not supply any modes, the VS project will generate all [permutations of modes](#mode). | ### Opening in an IDE #### Visual Studio / Rider -To generate Visual Studio project files, run the `xmake project -k vsxmake2022` command. +To generate Visual Studio project files, run the `xmake project -k vsxmake2022 -m "Game__Shipping__Win64"` command. Afterwards open the generated `.sln` file inside of the `vsxmake2022` directory Note that you should also commit & push the submodules that you've updated if the reason why you updated was not because someone else pushed an update, and you're just catching up to it. -**Note the following about how xmake interacts with VS** - +> [!WARNING] > The vs. build plugin performs the compile operation by directly calling the xmake command under vs, and also supports intellisense and definition jumps, as well as breakpoint debugging. - -This means that modifying the project properties within Visual Studio will not affect which flags are passed to the build when VS executes `xmake`. XMake provides some configurable project settings +> This means that modifying the project properties within Visual Studio will not affect which flags are passed to the build when VS executes `xmake`. XMake provides some configurable project settings which can be found in VS under the `Project Properties -> Configuration Properties -> Xmake` menu. +##### Configuring additional modes + +> [!TIP] +> Additional modes can be generated by running `xmake project -k vsxmake2022 -m "Game__Shipping__Win64,Game__Debug__Win64"`. +> [Further explanation can be found in the `xmake` command table](#helpful-xmake-commands). + +##### Regenerating solution best practices + +> [!CAUTION] +> If you change your configuration with `xmake config`, you *may* need to regenerate your Visual Studio solution to pick up on changes to your configuration. You can simply re-run the `xmake project -k vsxmake2022 -m ""` command to regenerate the solution. + ## Updating git submodules If you want to update git submodules, you do so one of three ways: diff --git a/deps/first/patternsleuth_bind/Cargo.lock b/deps/first/patternsleuth_bind/Cargo.lock index 9ee4d977d..dd07ac157 100644 --- a/deps/first/patternsleuth_bind/Cargo.lock +++ b/deps/first/patternsleuth_bind/Cargo.lock @@ -691,4 +691,4 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" name = "windows_x86_64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" \ No newline at end of file diff --git a/deps/first/patternsleuth_bind/Cargo.toml b/deps/first/patternsleuth_bind/Cargo.toml index 20cf69b68..25f01784b 100644 --- a/deps/first/patternsleuth_bind/Cargo.toml +++ b/deps/first/patternsleuth_bind/Cargo.toml @@ -4,4 +4,4 @@ version = "0.1.0" edition = "2021" [dependencies] -patternsleuth = { path = "../patternsleuth/patternsleuth", features = ["process-internal"] } +patternsleuth = { path = "../patternsleuth/patternsleuth", features = ["process-internal"] } \ No newline at end of file diff --git a/deps/first/patternsleuth_bind/xmake.lua b/deps/first/patternsleuth_bind/xmake.lua new file mode 100644 index 000000000..bd57b28a3 --- /dev/null +++ b/deps/first/patternsleuth_bind/xmake.lua @@ -0,0 +1,27 @@ +-- If patternsleuth is configured to install as a package. +add_requires("cargo::patternsleuth_bind", { optional = not is_config("patternsleuth", "package"), debug = is_mode_debug(), configs = { cargo_toml = path.join(os.scriptdir(), "Cargo.toml") } }) + +target("patternsleuth_bind") + set_kind("static") + set_values("rust.cratetype", "staticlib") + add_files("src/lib.rs") + -- Exposes the src/lib.rs files to the Visual Studio project filters. + add_extrafiles("src/lib.rs") + + -- If patternsleuth is configured to install as a package. + if is_config("patternsleuth", "package") then + add_packages("cargo::patternsleuth_bind") + add_links("ws2_32", "advapi32", "userenv", "ntdll", "oleaut32", "bcrypt", "ole32", { public = true }) + end + + -- If patternsleuth should be built as part of compilation. + if is_config("patternsleuth", "local") then + add_deps("patternsleuth") + add_rules("rust.link") + end + + -- We need to clean up the non-selected package/local version of patternsleuth when we reconfigure. + -- This just deletes the .lib and keeps cargo deps on disk for faster recompilation. + on_config(function(target) + os.tryrm(target:targetfile()) + end) \ No newline at end of file diff --git a/deps/first/xmake.lua b/deps/first/xmake.lua index 8f9999feb..875d9ce34 100644 --- a/deps/first/xmake.lua +++ b/deps/first/xmake.lua @@ -12,21 +12,29 @@ includes("LuaMadeSimple") includes("LuaRaw") includes("MProgram") includes("ParserBase") +includes("patternsleuth_bind") includes("Profiler") includes("ScopedTimer") includes("SinglePassSigScanner") includes("Unreal") --- Patternsleuth -> START +if is_config("patternsleuth", "local") then + -- The patternsleuth target is managed by the cargo.build rule. + target("patternsleuth") + set_kind("static") + add_rules("cargo.build", {project_name = "patternsleuth", is_debug = is_mode_debug(), features= { "process-internal" }}) + add_files("patternsleuth/Cargo.toml") + -- Exposes the rust *.rs files to the Visual Studio project filters. + add_extrafiles("patternsleuth/**.rs") +end -add_requires("cargo::patternsleuth_bind", { debug = is_mode_debug(), configs = { cargo_toml = path.join(os.scriptdir(), "patternsleuth_bind/Cargo.toml") } }) - -target("patternsleuth_bind") - set_kind("static") - set_values("rust.cratetype", "staticlib") - add_files("patternsleuth_bind/src/lib.rs") - add_packages("cargo::patternsleuth_bind") - - add_links("ws2_32", "advapi32", "userenv", "ntdll", "oleaut32", "bcrypt", "ole32", { public = true }) - --- Patternsleuth -> END +-- This option allows users to choose if patternsleuth should be installed as a package +-- or if patternsleuth should be built as a dependency by xmake. The `package` option +-- should be used if you don't intend on ever modifying the patternsleuth source. +-- The `local` option should be used if you want changes in the patternsleuth +-- submodule to be included as part of the UE4SS build. +option("patternsleuth") + set_default("package") + set_showmenu(true) + set_values("package", "local") + set_description("Install patternsleuth as a package or build it as a dependency.", "package", "local") diff --git a/tools/xmakescripts/modules/cargo/cargo_helpers.lua b/tools/xmakescripts/modules/cargo/cargo_helpers.lua new file mode 100644 index 000000000..f5f1c9525 --- /dev/null +++ b/tools/xmakescripts/modules/cargo/cargo_helpers.lua @@ -0,0 +1,63 @@ +---@alias rust_mode "dev" | "release" + +--- Takes a target and returns context variables to be used in on_xxx overrides. +---@param target any xmake target +---@param is_debug boolean Should we get the debug or release context? +---@return string cargo_dir The root cargo dir for the project. Ex. Intermediates/Cargo/target/ +---@return string cargo_mode_dir The cargo dir for the debug/release config. Ex. Intermediates/Cargo/target/debug/ +---@return rust_mode rust_mode The rust flavor of mode derrived from is_debug param. +function get_cargo_context(target, is_debug) + local rust_mode = is_debug and "dev" or "release" + local rust_mode_dir = is_debug and "debug" or "release" + local cargo_dir = path.join(get_config("buildir"), "cargo", target:name()) + return cargo_dir, path.join(cargo_dir, rust_mode_dir), rust_mode +end + +--- Parses ".d" files created by Cargo. +---@param file string Path to a ".d" file created by Cargo +---@return string[] cargo_deps A list of any src files that should trigger a rebuild. +function get_dependencies(file) + if not os.exists(file) then + return {} + end + + local data = io.readfile(file) + data = data:trim() + local start = data:find(": ", 1, false) + local deps = data:sub(start + 2):split(" ", {strict = false, plain = true}) + + local parsed_deps = {} + local dep = "" + local dep_idx = 1 + while dep do + dep = deps[dep_idx] + if dep then + while dep:endswith("\\") do + dep_idx = dep_idx + 1 + dep = dep:sub(1, -2) .. " " .. deps[dep_idx] + end + table.insert(parsed_deps, dep) + dep_idx = dep_idx + 1 + end + end + + return parsed_deps +end + +--[[ +The previous function is based on the cargo impl of internal .d parsing. + +let mut deps = line[pos + 2..].split_whitespace(); + +while let Some(s) = deps.next() { + let mut file = s.to_string(); + while file.ends_with('\\') { + file.pop(); + file.push(' '); + file.push_str(deps.next().ok_or_else(|| { + internal("malformed dep-info format, trailing \\".to_string()) + })?); + } + ret.files.push(file.into()); +} +]]-- \ No newline at end of file diff --git a/tools/xmakescripts/rules/rust_rules.lua b/tools/xmakescripts/rules/rust_rules.lua new file mode 100644 index 000000000..a22127e52 --- /dev/null +++ b/tools/xmakescripts/rules/rust_rules.lua @@ -0,0 +1,181 @@ +-- This rule extends xmake's default rust rules in order to support +-- precision linking. This rule makes rust report which static libs it expects +-- to be linked against and those libs are propagated up to anything else that +-- wants to link to the created staticlib. +rule("rust.link") + add_imports("core.project.depend") + + on_load(function(target) + -- Rust expects the native-static-libs dir to exist before we run `rustc`. + os.mkdir(target:dependir()) + + -- Save any native libraries we compiled with so we can add them as links later. + target:add("rcflags", "--print native-static-libs="..path.translate(path.join(target:dependir(), "native-libs.txt"))) + end) + + after_build(function(target) + -- After building, we need to add the `native-libs.txt` file to the .d file. + -- This allows xmake to detect if we need to rebuild if the native libs reported by rust + -- have been regenerated/changed. + local native_libs = path.join(target:dependir(), "native-libs.txt") + + if not os.exists(native_libs) then + raise("Cargo target: %s did not export a list of native libs to link against. Try `xmake build --rebuild %s`", target:name(), target:name()) + end + + local dependinfo = depend.load(target:dependfile()) or {} + table.append(dependinfo.files, native_libs) + -- There might be a way to avoid manual deduping of the dependency files to achieve + -- idempotency, but this should suffice for now. The list of files is small (two files for now). + dependinfo.files = table.unique(dependinfo.files) + depend.save(dependinfo, target:dependfile()) + + -- Load the native lib list that was generated during rustc invocation. + -- We add these native libs as links. + local data = io.readfile(native_libs) + local deps = data:split(" ", {plain = true}) + for _, lib in ipairs(deps) do + -- Windows has another quirk where we have to manually link against the msvcrtd. + -- Even though rust reports that it links against msvcrt, we have to link to the debug runtime. + if lib == "msvcrt.lib" then + if os.is_host("windows") and target:get("runtimes") == "MDd" then + target:add("links", "msvcrtd.lib", {public = true}) + end + else + target:add("links", lib, {public = true}) + end + end + end) + +-- This rule allows for generic pure-rust Cargo.toml projects to have their building +-- marshalled and managed entirely by the Cargo/rustc toolchain. +rule("cargo.build") + set_extensions(".toml") + add_imports("cargo.cargo_helpers", "core.project.depend") + + on_load(function (target) + -- Get the user supplied project to build from the cargo workspace. + local project_name = target:extraconf("rules", "cargo.build", "project_name") + assert(project_name, "No project_name passed to the cargo.build rule.") + + -- Convert the supplied project name to the rust output format. + -- ex. patternsleuth.lib -> libpatternsleuth.rlib + if target:is_static() then + target:set("basename", "lib" .. project_name:gsub("-", "_")) + target:set("extension", ".rlib") + end + + local is_debug = target:extraconf("rules", "cargo.build", "is_debug") + + local _, cargo_output_dir, _ = cargo_helpers.get_cargo_context(target, is_debug) + + -- Add framework dirs so any projects that add_deps() this target can get the proper rust flags. + -- frameworks are in the format of `--extern cratename=dir/to/crate.rlib` + target:add("frameworks", path.join(cargo_output_dir, target:filename()), {public = true}) + -- frameworkdirs are in the format of `-L dependency=dir/to/deps/` + target:add("frameworkdirs", path.join(cargo_output_dir, "deps"), {public = true}) + end) + + on_build_file(function (target, sourcefile, opt) + -- Windows has a quirk where rust will prefer to use the shipped .lib imports from the windows cargo packages. + -- This results in rust reporting that we have to link to windows0.48.5, etc. + -- Instead of having to parse the windows package dependencies and link to their pre-built .lib, + -- we use the windows_raw_dylib to allow rust to resolve and and create the imports automatically. + -- More background on this can be found at https://kennykerr.ca/rust-getting-started/understanding-windows-targets.html + if(os.is_host("windows")) then + os.setenv("RUSTFLAGS", "--cfg windows_raw_dylib") + end + + -- Get the user supplied project to build from the cargo workspace. + local project_name = target:extraconf("rules", "cargo.build", "project_name") + local is_debug = target:extraconf("rules", "cargo.build", "is_debug") + local cargo_dir, cargo_mode_dir, cargo_mode = cargo_helpers.get_cargo_context(target, is_debug) + local targetfile = path.join(cargo_mode_dir, target:filename()) + + local argv = { + "rustc", + "--manifest-path="..sourcefile, + -- Only support the cargo lib type for now. + "--lib", + "--profile", + cargo_mode, + "-p", + project_name, + "--crate-type", + "rlib", + "--target-dir="..cargo_dir, + "--quiet" + } + + -- Add any cargo features if we specified them in add_rules(). + local features = target:extraconf("rules", "cargo.build", "features") + if features then + table.append(argv, "-F", table.concat(features, ",")) + end + + -- Support the dry-run logic from xmake. + import("core.base.option") + local dryrun = option.get("dry-run") + + -- Parse cargo's dependent files list and add to this target's dependent files list. + -- This allows us to precisely add any .rs files that should trigger a rebuild. + local dependent_files = cargo_helpers.get_dependencies(path.join(cargo_mode_dir, target:basename().. ".d")) + -- Detect any changes to Cargo.toml. + table.join2(dependent_files, sourcefile) + -- Detect any changes to the xxx.rlib target file. + table.join2(dependent_files, path.join(cargo_mode_dir, target:filename())) + + -- The function in depend.on_changed() runs when xmake detects changes that will require a rebuild. + -- The conditions that trigger a rebuild are defined in on_changed(function(), { Conditions }). + depend.on_changed(function () + -- Ensure the user has cargo installed. + import("lib.detect.find_tool") + local cargo = find_tool("cargo") + if not cargo then + raise("cargo not found!") + end + + import("utils.progress") + progress.show(opt.progress or 0, "${color.build.object}compiling.cargo %s", sourcefile) + + if not dryrun then + os.execv(cargo.program, argv) + end + end, { dependfile = target:dependfile(target:targetfile()), + lastmtime = os.mtime(targetfile), + changed = target:is_rebuilt(), + values = argv, + files = dependent_files, + dryrun = dryrun}) + end) + + before_clean(function(target) + local is_debug = target:extraconf("rules", "cargo.build", "is_debug") + local cargo_dir, _, cargo_mode = cargo_helpers.get_cargo_context(target, is_debug) + local project_name = target:extraconf("rules", "cargo.build", "project_name") + + import("lib.detect.find_tool") + local cargo = find_tool("cargo") + if not cargo then + raise("cargo not found!") + end + + -- We use the cargo clean command to clean cargo built targets instead of just nuking the deps folder. + -- This allows for faster builds since we can safely cache most of the rust dependency crates. + for _, sourcefile in ipairs(target:sourcefiles()) do + local argv = { + "clean", + "-p", + project_name, + "--target-dir="..cargo_dir, + "--manifest-path="..sourcefile, + "--profile", + cargo_mode + } + os.execv(cargo.program, argv) + end + end) + + -- Cargo manages linking when building .rlib files. + -- Override the on_link function to give full control to this cargo rule. + on_link(function(target) end) \ No newline at end of file diff --git a/xmake.lua b/xmake.lua index 1af99b7c8..71de46775 100644 --- a/xmake.lua +++ b/xmake.lua @@ -1,3 +1,4 @@ +set_xmakever("2.9.2") -- We should use `get_config("ue4ssRoot")` instead of `os.projectdir()` or `$(projectdir)`. -- This is because os.projectdir() will return a higher parent dir -- when UE4SS is sub-moduled/`include("UE4SS")` in another xmake project. @@ -11,8 +12,8 @@ set_config("buildir", "Intermediates") -- /modules/rules/my_module.lua import("rules.my_module") add_moduledirs("tools/xmakescripts/modules") --- Load the build_rules file into the global scope. -includes("tools/xmakescripts/rules/build_rules.lua") +-- Load our rule files into the global scope. +includes("tools/xmakescripts/rules/**.lua") -- Generate the mode rules. local modes = generate_compilation_modes() @@ -36,7 +37,7 @@ set_allowedplats("windows") set_allowedarchs("x64") set_allowedmodes(modes) -if is_plat("windows") then +if is_host("windows") then set_defaultmode("Game__Shipping__Win64") end