Build Pipeline
How a Unity project becomes distributable .ait Explains what the SDK does internally until it becomes a package.
Audience: SDK contributors. If your goal is to build a game using the SDK, Build profileand Build customizationthis is the document you need.
2-stage pipeline structure
The build is divided into the stage where Unity creates the WebGL output, and the stage where that output is rearranged into a web project and packaged with granite.
┌─────────────────────────────────────────────────────────────────────┐
│ Entry Points │
│ Menu: Build & Package │ Build Window │ Server Start/Restart │
│ AppsInTossMenu.cs │ AppsInTossBuildWindow.cs │
└─────────────┬───────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ AITConvertCore.DoExport(buildWebGL, doPackaging, cleanBuild, │
│ profile, profileName) │
│ or DoExportAsync(...) │
└─────────────┬───────────────────────────────────────────────────────┘
│
┌────────┴────────────────────────────────┐
▼ ▼
┌──────────────────────┐ ┌────────────────────────────────┐
│ Phase 1: WebGL Build │ │ Phase 2: Packaging │
│ BuildWebGL() │ │ GenerateMiniAppPackage() │
│ │ │ → AITPackageBuilder │
│ - Init() │ │ .PackageWebGLBuild() │
│ - BuildPipeline │ │ │
│ - .ait-build-info │ │ 2a. Copy BuildConfig │
│ │ │ 2b. Copy WebGL → public │
│ Output: webgl/ │ │ 2c. Replace placeholders │
│ │ │ 2d. Insert loading screen │
│ │ │ 2e. pnpm install │
│ │ │ 2f. granite build │
│ │ │ │
│ │ │ Output: ait-build/dist/ │
└──────────────────────┘ └────────────────────────────────┘Call matrix
Build & Package
true
true
false
Build & Package (clean)
true
true
true
Deploy (Test)
true
true
false
Deploy (Production)
true
true
true
Dev Server Start
true
true
false
Restart Server
true
true
false
Restart (server-only)
—
—
—
Note:
Restart (server-only)isDoExportrestarts only the granite process without calling it.
Phase 0 initialization
Template synchronization
AITTemplateManager.EnsureWebGLTemplatesExistcopies the SDK's WebGL templates into the project before building.
SDK template search order:
Packages/im.toss.apps-in-toss-unity-sdk/WebGLTemplates/Packages/com.appsintoss.miniapp/WebGLTemplates/Assembly path-based (
typeof(AITConvertCore).Assembly.Location)
If it does not exist in the project, Assets/WebGLTemplates/AITTemplate/it copies everything; if it does exist, it updates based on markers to preserve user custom areas. Below template merge timing section.
Build settings
AITBuildInitializer.Initautomatically configures Unity PlayerSettings.
WebGL Template
PROJECT:AITTemplate
hard-coded
Linker Target
Wasm
hard-coded
Scripting Backend
IL2CPP
hard-coded
Memory Size
256~1536MB
Default values by Unity version (user override possible)
Compression
Brotli
Default value. decompressionFallbackis enabled, so it is available in all Unity versions
Threading
false
Default value (mobile browser compatibility)
Data Caching
false
Default
nameFilesAsHashes
user setting (default true)
forced only in Unity 2021.x false — truecauses a Bee build loop bug
Engine Code Stripping
user setting
—
Managed Stripping
High
Default
IL2CPP Config
user setting
—
The single source of truth for default values is AITEditorScriptObjectof GetDefault* static methods. Only the Dev Server profile lowers compression Disabledto this — profile-specific differences are Build profiledocumented there.
Default memory by version:
Unity 2021.3: 256MB
Unity 2022.3: 512MB
Unity 6 (2023.3+): 1024MB
Unity 2024.2+: 1536MB
Environment variable overrides applied to profiles are AITBuildInitializer.ApplyEnvironmentVariableOverrideshandles them. The list of variables and their values Build profileis the source of truth.
Config validation
DoExporton startup UnityUtil.GetEditorConf()reads the settings asset, and if the asset itself cannot be found, INVALID_APP_CONFIGis returned.
An empty app ID or icon URL does not block the build. The app ID is only a condition that disables the build button in the Configuration window (AITEditorScriptObject.IsAppNameValid), and the icon URL is only format-checked when entered. In other words, if you build with empty values as-is, %AIT_ICON_URL% and similar values become packages with empty strings substituted.
Phase 1 WebGL build
AITConvertCore.BuildWebGL()
execution flow
Build marker
After a successful WebGL build webgl/.ait-build-info.jsonrecords metadata in. The schema is AITConvertCore.csof AITBuildInfo class.
sdkVersion
SDK package version
buildTime
UTC ISO 8601 build time
compressionFormat
PlayerSettings.WebGL.compressionFormat int value (0=Disabled, 1=Gzip, 2=Brotli)
decompressionFallback
PlayerSettings.WebGL.decompressionFallback. If enabled, the output extension becomes .unitywebbecomes
profileName
"Development" or "Production"
unityVersion
Application.unityVersion
ReadBuildMarker(webglPath)reads the marker and returns AITBuildInfo, and if the file does not exist or parsing fails, nullreturn.
marker use cases:
build cache validation (
ShouldForceCleanBuild) — auto clean build if Unity version mismatches or there is no markercompression format detection (
CopyWebGLToPublic) —compressionFormatanddecompressionFallbackaccurate extension determined by
build cache integrity check
ShouldForceCleanBuild(outputPath, cleanBuild)validates the existing cache before the WebGL build.
Unity's BuildPipelineperforms incremental builds by default, so webgl/if it remains, only changed assets are recreated. cleanBuild=trueif it is webgl/deletes BuildOptions.CleanBuildCacheand performs a full build.
Phase 2 packaging
AITPackageBuilder.PackageWebGLBuild() (sync) or PackageWebGLBuildAsync() (async). Both paths share common preparation logic through PreparePackaging()to share common preparation logic.
Copy BuildConfig
CopyBuildConfigFromTemplateof this SDK WebGLTemplates/AITTemplate/BuildConfig~/the ait-build/copies to.
package.json
merge dependencies
tsconfig.json
merge compilerOptions
vite.config.ts
%AIT_VITE_HOST%, %AIT_VITE_PORT% replace
granite.config.ts
replace 13 placeholders
apps-in-toss.config.ts
3.x configuration file. granite.config.tsplaceholder set such as
pnpm-lock.yaml
copy if present
The actual merge rules are in Package/BuildConfigMerger.cshere.
Copy WebGL to public
CopyWebGLToPublicIf it is webgl/ rearranges the output into a ait-build/ structure.
During this process, placeholder replacement and loading screen insertion happen together. The replacement rules are in the section below Placeholder replacement section. The behavior and customization of the loading screen itself are Loading screen customizationis the source of truth.
pnpm install
Determines whether installation can be skipped. To avoid rerunning install on every build, immediately after a successful install Package/PnpmInstallStateMarker.csis package.json·pnpm-lock.yamlstores the content hash and pnpm version of ait-build/node_modules/.ait-install-state.jsonin. On the next build, if all these values match and node_modules the integrity check passes, install is skipped.
The reason the marker is node_modules placed inside is that NodeModulesValidator.CleanNodeModulesis node_modulesif it is deleted entirely, the marker is invalidated as well — automatically aligning with the clean stage of the retry policy. Any case where determination is impossible, such as when there is no marker or parsing fails, is handled fail-closed as "cannot skip." This is because the cost of an incorrect skip (build failure) is greater than the cost of an unnecessary reinstall (wasted time).
The kill switch is the environment variable AIT_DISABLE_INSTALL_SKIP. 1/trueIf set, skipping is disabled, and if the value cannot be interpreted, a warning is logged and skipping is disabled as well — it behaves fail-safe so a typo cannot neutralize the kill switch.
3-stage retry. If not skipped PnpmInstallStages proceeds in the order defined in the array.
ValidateNodeModulesIntegrity()validation order:
node_modules/valid if absent (newly installed)node_modules/.pnpm/invalid if the directory does not exist (stale modules)package.jsonin@apps-in-toss/web-frameworkversion extractionnode_modules/.pnpm/@apps-in-toss+web-framework@{version}*/existence check — if version mismatch or package is missing, warn and mark invalid
granite build
if it fails CleanNodeModules() → pnpm install --no-frozen-lockfile → pnpm run buildretry once, and if it still fails, FAIL_NPM_BUILDis returned.
The output is ait-build/dist/and if the file is missing here, .ait file missing DIST_FOLDER_MISSING or AIT_FILE_MISSINGleads to.
File signature detection and validation
AITBuildValidatorverifies the existence and integrity of the WebGL output.
Search patterns by compression format
GetFilePatterns(compressionFormat, decompressionFallback)Determines the search patterns based on this build marker value.
decompressionFallback = true
*.data.unityweb
*.framework.js.unityweb
*.wasm.unityweb
0 Disabled
*.data
*.framework.js
*.wasm
1 Gzip
*.data.gz
*.framework.js.gz
*.wasm.gz
2 Brotli
*.data.br
*.framework.js.br
*.wasm.br
Other (fallback)
*.data*
*.framework.js*
*.wasm*
decompressionFallbackIf this is enabled, it takes precedence over the compression format. Since the loader is not a compression target, it is always *.loader.js.
File detection
FindFileInBuild(buildPath, pattern, isRequired)Finds files with a glob pattern. *.data* The same trailing wildcard also *.data.metamatches, so .metais excluded from the results — if it is not excluded, LastWriteTime in sorting, .metais selected as the newest and the wrong file name is returned.
*.loader.js
Yes
Unity WebGL loader
*.data*
Yes
Game data
*.framework.js*
Yes
Unity framework
*.wasm*
Yes
WebAssembly binary
*.symbols.json*
No
debug symbols
Auto-clean on duplicate matches. If multiple files match one pattern, LastWriteTime they are sorted in descending order (and by file name descending on ties), leaving only the newest one and deleting the rest .metawith it. The approach of only leaving warning logs was changed to deletion because it happened repeatedly on every build and accumulated Sentry noise. If any file fails to be deleted, an informational log recommending a Clean Build is written.
Required file missing. isRequired=trueIf a pattern cannot be found, only the first line is sent to Sentry (so fingerprints are stably grouped by pattern), and the remaining diagnostic lines are left only in the console. Diagnostics include the search path, the following strings, and the actual file list in the Build folder (or that it is empty).
The return value is an empty string, and the caller REQUIRED_FILE_MISSINGleads to.
Placeholder substitution validation
ValidatePlaceholderSubstitution(content, filePath)This regex %[A-Z_]+%finds unsubstituted placeholders.
Critical (error + build failure):
%UNITY_WEBGL_LOADER_URL%%UNITY_WEBGL_DATA_URL%%UNITY_WEBGL_FRAMEWORK_URL%%UNITY_WEBGL_CODE_URL%
Others %...% patterns are only warnings. The following empty path patterns are also treated as critical:
apps-in-toss.config.tsIn this case, unsubstituted placeholders in the SDK_GENERATED area are hard errors, but SDK placeholders left in the USER_CONFIG area or keys moved in 3.x are only warnings — when merged, SDK values take precedence, so the build result is normal.
Build completion report
PrintBuildReport(buildProjectPath, distPath)is ait-build/public/Build/scans and prints whether 4 required patterns and 1 optional pattern exist, along with file sizes. If a required pattern is missing, Debug.LogErroras a [Missing!]is displayed, and optional patterns are displayed only when present.
Placeholder replacement
index.html
AITPackageBuilder.CopyWebGLToPublic()is performed in.
Unity standard
%UNITY_WEB_NAME%
PlayerSettings.productName
%UNITY_WIDTH%
PlayerSettings.defaultWebScreenWidth
%UNITY_HEIGHT%
PlayerSettings.defaultWebScreenHeight
%UNITY_COMPANY_NAME%
PlayerSettings.companyName
%UNITY_PRODUCT_NAME%
PlayerSettings.productName
%UNITY_PRODUCT_VERSION%
PlayerSettings.bundleVersion
Unity WebGL URL — build will fail if not substituted.
%UNITY_WEBGL_LOADER_URL%
Build/{loaderFile}
%UNITY_WEBGL_DATA_URL%
Build/{dataFile}
%UNITY_WEBGL_FRAMEWORK_URL%
Build/{frameworkFile}
%UNITY_WEBGL_CODE_URL%
Build/{wasmFile}
%UNITY_WEBGL_SYMBOLS_URL%
Build/{symbolsFile} (or empty string)
Legacy file names — for backward compatibility. Only the file name is substituted, without the path.
%UNITY_WEBGL_LOADER_FILENAME%
{loaderFile}
%UNITY_WEBGL_DATA_FILENAME%
{dataFile}
%UNITY_WEBGL_FRAMEWORK_FILENAME%
{frameworkFile}
%UNITY_WEBGL_CODE_FILENAME%
{wasmFile}
%UNITY_WEBGL_SYMBOLS_FILENAME%
{symbolsFile}
AIT custom
%AIT_ENABLE_DEBUG_CONSOLE%
"true" / "false"
Enable debug console
%AIT_DEVICE_PIXEL_RATIO%
Number
Device pixel ratio
%AIT_ICON_URL%
URL string
App icon URL
%AIT_DISPLAY_NAME%
String
App display name
%AIT_PRIMARY_COLOR%
Color code
Brand color (default: #3182f6)
%AIT_PRELOAD_TAGS%
HTML tag
<link rel="preload"> Tag
Preload tags
GeneratePreloadTags(dataFile, wasmFile, frameworkFile)is generated.
Important: framework.js is not preloaded. If the Unity loader loads framework.js
<script>with a tag,as="fetch"the preload and cache keys may not match, causing duplicate downloads. This increases memory pressure and raises the probability of intermittent initialization failures (ASM_CONSTS errors).
granite.config.ts
Package.BuildConfigMerger.UpdateGraniteConfig()replaces 13 items.
%AIT_APP_NAME%
config.appName
%AIT_DISPLAY_NAME%
config.displayName
%AIT_PRIMARY_COLOR%
config.primaryColor
%AIT_ICON_URL%
config.iconUrl
%AIT_BRIDGE_COLOR_MODE%
config.GetBridgeColorModeString()
%AIT_WEBVIEW_TYPE%
config.GetWebViewTypeString()
%AIT_NAVIGATION_BAR%
config.GetNavigationBarJson()
%AIT_ALLOWS_INLINE_MEDIA_PLAYBACK%
config.allowsInlineMediaPlayback
%AIT_MEDIA_PLAYBACK_REQUIRES_USER_ACTION%
config.mediaPlaybackRequiresUserAction
%AIT_VITE_HOST%
config.viteHost
%AIT_VITE_PORT%
config.vitePort
%AIT_PERMISSIONS%
config.GetPermissionsJson()
%AIT_OUTDIR%
config.outdir
vite.config.ts
%AIT_VITE_HOST% → config.viteHost, %AIT_VITE_PORT% → config.vitePort.
template merge timing
AITTemplateManagermerges the SDK template and project custom area based on markers. The marker syntax and the editable areas are Build customizationthe source of truth. Here, the merge is when and what happens only.
The merge occurs in Phase 0 EnsureWebGLTemplatesExistat, that is, before the Unity WebGL build starts.
If an older version of index.html without markers is found, the following warning is issued and it is replaced with the SDK template.
Node.js and pnpm management
The SDK downloads and uses its own Node.js regardless of the system installation. The single source of truth for versions is AITNodeJSDownloader.csof NODE_VERSIONand AITPackageManagerHelper.csof PNPM_VERSION.
PNPM_VERSIONis package.json, sdk-runtime-generator~/package.json, WebGLTemplates/AITTemplate/BuildConfig~/package.json The packageManager field in the three places must always be the same. If the values diverge, the pnpm used by the client and the pnpm that updated the lockfile differ, causing specifier drift.
The installation path is ~/.ait-unity-sdk/nodejs/v{NODE_VERSION}/{platform}/.
Download mirrors fall back in order.
https://nodejs.org/dist/(official)https://cdn.npmmirror.com/binaries/node/https://repo.huaweicloud.com/nodejs/
Platform-specific SHA256 hashes are AITNodeJSDownloader.cshardcoded in. Node.js and pnpm executable path resolution and process management are handled by AITPackageManagerHelperis responsible for this.
Error code
AITConvertCore.AITExportError is an enum. The value 7was previously WEBGL_BUILD_INCOMPLETEbut 10~13was removed when it was refined into more detailed values.
SUCCEED
0
Success
NODE_NOT_FOUND
1
No Node.js
BUILD_WEBGL_FAILED
2
WebGL build error
INVALID_APP_CONFIG
3
App settings error
NETWORK_ERROR
4
Network error
CANCELLED
5
User canceled
FAIL_NPM_BUILD
6
pnpm build error
BUILD_FOLDER_MISSING
10
No Build folder
REQUIRED_FILE_MISSING
11
Required file missing
INDEX_HTML_MISSING
12
No index.html
PLACEHOLDER_SUBSTITUTION_FAILED
13
Placeholder not substituted
DIST_FOLDER_MISSING
14
No dist folder
AIT_FILE_MISSING
15
No .ait file
The full message shown to the user and the short label are both owned by AITExportErrorCatalogowns them.
User warnings and dialog conditions
Error dialog
No SDK loading template
"Error"
Could not find the SDK loading screen template
No Build folder
"Error"
Could not find the WebGL build folder
App name not set
"Error"
The app name is not set
Deployment key not set
"Error"
The deployment key is not set
pnpm installation failed
"Build failed"
Failed to install pnpm
Build canceled
"Canceled"
The build was canceled
Build succeeded
"Success"
Build and packaging completed
Clean completed
"Done"
Clean completed successfully
Deployment timeout
"Timeout"
Deployment timed out
Port conflict
"Port conflict"
That port is already in use
Confirmation dialog
AIT/Clean
"Would you like to delete the webgl/ and ait-build/ folders?"
Deployment confirmation
"App name: X, version: Y — Deploy?" (memo auto-generated value displayed)
Reset settings
"Would you like to reset the settings?"
Reset loading screen
"Would you like to reset the loading screen to the default template?"
3-way dialog
Build failed
"OK" / "Report Issue"
Deployment failed
"OK" / "Report Issue"
Console warning
Non-critical placeholder not substituted
Display the placeholder name
SDK-managed settings remain in USER_CONFIG
Deletion recommended (build is normal)
pnpm install --frozen-lockfile Failure
Proceed to the next retry step
web-framework version mismatch
Expected vs actual version display
node_modules/.pnpm None
stale modules
Upgrading previous version templates
Instructions for manually reapplying custom changes
No loading screen file
An empty loading screen is used
Failed to write build marker
Warnings only (build continues)
No build marker / Unity version mismatch
Automatic clean build
AIT_DISABLE_INSTALL_SKIP Unable to parse value
Treated as skip disabled
Console error
webgl/Build/ No folder
BUILD_FOLDER_MISSING
Required WebGL file missing
REQUIRED_FILE_MISSING
index.html None
INDEX_HTML_MISSING
Fatal placeholder not replaced
PLACEHOLDER_SUBSTITUTION_FAILED
Empty path pattern detected
PLACEHOLDER_SUBSTITUTION_FAILED
No dist after granite build
DIST_FOLDER_MISSING
In dist .ait None
AIT_FILE_MISSING
Final pnpm install failure
Build aborted
No SDK BuildConfig folder
Build aborted
No SDK WebGLTemplates folder
Build aborted
Server lifecycle
The local server is only the Dev Server (the Production Server that used to exist was removed starting from 3.0.0 because it became impossible to connect sandbox apps — to verify production settings on a real device, Deploy (Test) in Getting Starteduse it).
StartServer()
Build + start server
StopServer()
Terminate server process
RestartServer(serverOnly)
serverOnly=falsethen build + server, truethen restart server only
If the target port is already in use, show a "port conflict" dialog, and the user must change the port or terminate the occupying process.
Error reporting
AITErrorReporteris [InitializeOnLoad]When the editor starts, with Application.logMessageReceivedsubscribes to and captures all console logs in a circular buffer.
errorLogs
50
LogType.Error, LogType.Exception
warningLogs
30
LogType.Warning
infoLogs
20
LogType.Log, LogType.Assert
OpenIssueInBrowser(errorCode, profileName)automatically constructs the GitHub Issue URL from this buffer. The title is [Build Error] {errorCode}and the body includes the SDK/Unity/OS versions, profile name, error code and message, app settings, BuildReport errors (if any), and the most recent console logs. If the URL exceeds 2000 characters, infoLogs → warningLogs → errorLogs it is trimmed step by step in that order.
The scope of logs sent to Sentry and the noise-suppression policy are Sentry integrationhere.
Related documents
Build profile — per-profile setting differences, environment variable overrides
Build customization — marker area contract, web entry point editing
Loading screen customization — loading screen replacement and
AITLoadingAPISentry integration — error collection and context injection
Troubleshooting — when the build gets stuck
Was this helpful?