分析UBT中EULA的内容分发限制

The EULA License Grant/(A) of UE clearly states that content developed using UE and redistributed must not include uncooked source formats or paid content developed based on engine tools. This article studies the specific content distribution details in the EULA and how to technically circumvent this limitation.

There is no restriction on your Distribution of a Product made using the Licensed Technology that does not include any Engine Code or any Paid Content Distributed in uncooked source format (in each case, including as modified by you under the License) and does not require any Licensed Technology (including as modified by you under the License) to run (“Unrestricted Products”). For clarity, the foregoing does not constitute a license under any patents, copyrights, trademarks, trade secrets or other intellectual property rights, whether by implication, estoppel or otherwise.

The definition of Engine Tools in the EULA is:

“Engine Tools” means (a) editors and other tools included in the Engine Code; (b) any code and modules in either the Developer or Editor folders, including in object code format, whether statically or dynamically linked; and (c) other software that may be used to develop standalone products based on the Licensed Technology.

This means developers cannot use any modules under Editor or Developer in game content.
The user agreement is a legally binding document. I intend to tinker a bit and also take a look at how UE implements this limitation, solely for the purpose of record analysis, and will not engage in commercial redistribution or use it in formal projects. I read through the logic in UBT, and the code only needs a few simple modifications.

Note: Doing this in a formal release version violates the UE’s EULA, and it is strongly discouraged in official projects.
The code and compilation in this article are based on UE_4.18 (engine compiled from source locally), and the operations described in this article do not apply to engines installed from EpicLauncher, the reasons will be noted at the end.

In UE, when the packaged game (Target is Game) is Shipping, if the project contains Editor/Developer modules, the following error will be reported:

1
UATHelper: Packaging (Windows (64-bit)):   ERROR: ERROR: Non-editor build cannot depend on non-redistributable modules. C:\Users\imzlp\Documents\Unreal Projects\EULA418\Binaries\Win64\EULA418-Win64-Shipping.exe depends on 'DesktopPlatform'.

This means the DesktopPlatform is a non-redistributable module, and cannot be used in Game. By reviewing the UBT code, this exception is thrown in UBT’s CheckForEULAViolation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Source/Programs/UnrealBuildTools/Configuration/UEBuildTarget.cs
private void CheckForEULAViolation()
{
if (TargetType != TargetType.Editor && TargetType != TargetType.Program && Configuration == UnrealTargetConfiguration.Shipping &&
Rules.bCheckLicenseViolations)
{
bool bLicenseViolation = false;
foreach (UEBuildBinary Binary in AppBinaries)
{
List<UEBuildModule> AllDependencies = Binary.GetAllDependencyModules(true, false);
IEnumerable<UEBuildModule> NonRedistModules = AllDependencies.Where((DependencyModule) =>
!IsRedistributable(DependencyModule) && DependencyModule.Name != AppName
);

if (NonRedistModules.Count() != 0)
{
IEnumerable<UEBuildModule> NonRedistDeps = AllDependencies.Where((DependantModule) =>
DependantModule.GetDirectDependencyModules().Intersect(NonRedistModules).Any()
);
string Message = string.Format("Non-editor build cannot depend on non-redistributable modules. {0} depends on '{1}'.", Binary.ToString(), string.Join("', '", NonRedistModules));
if (NonRedistDeps.Any())
{
Message = string.Format("{0}\nDependant modules '{1}'", Message, string.Join("', '", NonRedistDeps));
}
if(Rules.bBreakBuildOnLicenseViolation)
{
Log.TraceError("ERROR: {0}", Message);
}
else
{
Log.TraceWarning("WARNING: {0}", Message);
}
bLicenseViolation = true;
}
}
if (Rules.bBreakBuildOnLicenseViolation && bLicenseViolation)
{
throw new BuildException("Non-editor build cannot depend on non-redistributable modules.");
}
}
}

The key judgment is the IsRedistributable method (conceptually similar to a member function in C++):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Source/Programs/UnrealBuildTools/Configuration/UEBuildTarget.cs
public static bool IsRedistributable(UEBuildModule Module)
{
if(Module.Rules != null && Module.Rules.IsRedistributableOverride.HasValue)
{
return Module.Rules.IsRedistributableOverride.Value;
}
if(Module.RulesFile != null)
{
return !Module.RulesFile.IsUnderDirectory(UnrealBuildTool.EngineSourceDeveloperDirectory) && !Module.RulesFile.IsUnderDirectory(UnrealBuildTool.EngineSourceEditorDirectory);
}

return true;
}

The judgment here is whether the module belongs to Editor/Developer; if so, it returns true, leading to the exception being thrown later. For testing, I directly modified it to return true;.

However, even after making just that change, the packaging still fails, but now the error changes to a linking error. Continuing to look at the UBT code, I find another place that makes a check for the Shipping mode (the // ... indicates omitted unrelated code):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Source/Programs/UnrealBuildTools/Configuration/UEBuildTarget.cs
protected void AddPrecompiledModules()
{
// ...
bool bAllowDeveloperModules = false;
if(Configuration != UnrealTargetConfiguration.Shipping)
{
Directories.Add(UnrealBuildTool.EngineSourceDeveloperDirectory);
bAllowDeveloperModules = true;
}

// Find all the modules that are not part of the standard set
HashSet<string> FilteredModuleNames = new HashSet<string>();
foreach (string ModuleName in ModuleNames)
{
FileReference ModuleFileName = RulesAssembly.GetModuleFileName(ModuleName);
if (Directories.Any(x => ModuleFileName.IsUnderDirectory(x)))
{
string RelativeFileName = ModuleFileName.MakeRelativeTo(UnrealBuildTool.EngineSourceDirectory);
if (ExcludeFolders.All(x => RelativeFileName.IndexOf(x, StringComparison.InvariantCultureIgnoreCase) == -1) && !PrecompiledModules.Any(x => x.Name == ModuleName))
{
FilteredModuleNames.Add(ModuleName);
}
}
}

// Add all the plugins which aren't already being built
foreach(UEBuildPlugin PrecompilePlugin in PrecompilePlugins)
{
foreach (ModuleDescriptor ModuleDescriptor in PrecompilePlugin.Descriptor.Modules)
{
if (ModuleDescriptor.IsCompiledInConfiguration(Platform, TargetType, bAllowDeveloperModules && Rules.bBuildDeveloperTools, Rules.bBuildEditor, Rules.bBuildRequiresCookedData))
{
string RelativeFileName = RulesAssembly.GetModuleFileName(ModuleDescriptor.Name).MakeRelativeTo(UnrealBuildTool.EngineDirectory);
if (!ExcludeFolders.Any(x => RelativeFileName.Contains(x)) && !PrecompiledModules.Any(x => x.Name == ModuleDescriptor.Name))
{
FilteredModuleNames.Add(ModuleDescriptor.Name);
}
}
}
}
// ...
}

Comment out the above part regarding bAllowDeveloperModules, then change it to:

1
2
Directories.Add(UnrealBuildTool.EngineSourceDeveloperDirectory);
bool bAllowDeveloperModules=true;

Recompiling UBT is sufficient.

Now I can write a simple example to test, such as using the DesktopPlatform from Developer to call the system’s file selection.
First, add the DesktopPlatform module dependency in *.build.cs:

1
2
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" ,"DesktopPlatform"});
PrivateDependencyModuleNames.AddRange(new string[] { "DesktopPlatform" });

Then write a simple function exposed to Blueprints:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// .h
UCLASS()
class EULA418_API UFileOperatorFlib : public UBlueprintFunctionLibrary
{
GENERATED_BODY()

UFUNCTION(BlueprintCallable)
static FString ExtendGetOpenFileName();

};
// .cpp
#include "FileOperatorFlib.h"
#include "Developer/DesktopPlatform/Public/DesktopPlatformModule.h"
#include "Paths.h"

FString UFileOperatorFlib::ExtendGetOpenFileName()
{
IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get();

if (DesktopPlatform)
{
TArray<FString> OpenFilenames;
const bool bOpened = DesktopPlatform->OpenFileDialog(
nullptr,
TEXT("OpenFileDialog"),
FString(TEXT("")),
TEXT(""),
TEXT("All Files (*.*)"),
EFileDialogFlags::None,
OpenFilenames
);

if (OpenFilenames.Num() > 0)
{
return FPaths::ConvertRelativePathToFull(OpenFilenames[0]);
}
}
return TEXT("");
}

Under normal circumstances, packaging the above code as Shipping will produce the initial error mentioned in the article, but after our modifications, it can package successfully and execute correctly. This simple project and its packaged content can be downloaded here.
After launching, press the number key 1 (not Num 1) to open the Windows file selection window, and after selecting, the path of the chosen file will be displayed on the screen.

Note: You need to use the source version of the engine to compile the project. If you encounter a noexcept compilation error, you can add bForceEnableExceptions = true; in the project’s *.target.cs.
Recompiling the project will compile the modules in the engine (opening the folder of the engine installed from EpicLauncher will show that the default Editor/Developer modules have not compiled static libraries, which is why you cannot execute the operations described in this article using the engine installed from EpicLauncher).

The article is finished. If you have any questions, please comment and communicate.

Scan the QR code on WeChat and follow me.

Title:分析UBT中EULA的内容分发限制
Author:LIPENGZHA
Publish Date:2019/08/24 14:15
World Count:3.6k Words
Link:https://en.imzlp.com/posts/9050/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!