Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8d345e2
add in explict assets from build layout files
timt-unity3d May 29, 2025
bb1168f
added addressables build reports
timt-unity3d Jun 30, 2025
e308210
initial import of several additional commands
timt-unity3d Jul 7, 2025
6196d23
add all addressable tables
timt-unity3d Jul 8, 2025
3134ff5
move to serialized file commands
timt-unity3d Sep 8, 2025
015472e
move DDL into individual abstract commands
timt-unity3d Sep 10, 2025
6206b19
refactor sql files to individual files
timt-unity3d Sep 29, 2025
8925e43
Add separate parsers
timt-unity3d Oct 1, 2025
0534f4d
fix initialization
timt-unity3d Oct 1, 2025
2ae27eb
added more thorough documentation
timt-unity3d Oct 2, 2025
f369382
Merge branch 'main' into add-buildlayout-support
timt-unity3d Oct 2, 2025
8303ae0
typo fixes from code reviews
timt-unity3d Oct 2, 2025
8a58036
add missing build file
timt-unity3d Oct 7, 2025
180ca08
standardize imports
timt-unity3d Oct 14, 2025
c6aea2a
add comments about model
timt-unity3d Oct 14, 2025
38b6b64
fix documentation and view naming
timt-unity3d Oct 21, 2025
776a3a8
renamed from addr_ to addressables_
timt-unity3d Oct 21, 2025
ac3b968
small reorg
timt-unity3d Oct 21, 2025
c2d178d
Update Documentation/addressables-build-reports.md
timt-unity3d Oct 21, 2025
9f3a97f
Update Documentation/addressables-build-reports.md
timt-unity3d Oct 21, 2025
0c1cc00
Update Analyzer/SQLite/Parsers/Models/BuildLayout.cs
timt-unity3d Oct 21, 2025
b3ee976
Update Analyzer/SQLite/Parsers/Models/BuildLayout.cs
timt-unity3d Oct 21, 2025
31eac72
fixed output formatting
timt-unity3d Oct 21, 2025
1fdf1b9
Update Analyzer/AnalyzerTool.cs
timt-unity3d Oct 21, 2025
4954bab
Update Analyzer/Properties/Resources.resx
timt-unity3d Oct 21, 2025
3ee058a
Update Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesB…
timt-unity3d Oct 21, 2025
4348df9
Update Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesB…
timt-unity3d Oct 21, 2025
2714ad1
Update Analyzer/SQLite/Commands/AddressablesBuildReport/AddressablesB…
timt-unity3d Oct 21, 2025
e3a051c
Update Analyzer/SQLite/Parsers/Models/BuildLayout.cs
timt-unity3d Oct 21, 2025
686bd48
Update Analyzer/SQLite/Writers/AddressablesBuildLayoutSQLWriter.cs
timt-unity3d Oct 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ UnityFileSystemTestData/**/*.sln
UnityFileSystemTestData/ProjectSettings/
UnityFileSystemTestData/UserSettings/
UnityFileSystemTestData/Packages/
*.db
*.txt
*.csv
7 changes: 6 additions & 1 deletion Analyzer/Analyzer.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
Expand All @@ -15,8 +15,13 @@

<ItemGroup>
<PackageReference Include="Microsoft.Data.SQLite" Version="9.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>

<ItemGroup>
<Using Include="Analyzer.Properties" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\UnityFileSystem\UnityFileSystem.csproj" />
</ItemGroup>
Expand Down
219 changes: 57 additions & 162 deletions Analyzer/AnalyzerTool.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
using System;
using Analyzer.SQLite.Parsers;
using Analyzer.SQLite.Writers;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using UnityDataTools.Analyzer.SQLite;
using UnityDataTools.Analyzer.Build;
using UnityDataTools.Analyzer.SQLite.Handlers;
using UnityDataTools.FileSystem;

namespace UnityDataTools.Analyzer;
Expand All @@ -11,6 +16,12 @@ public class AnalyzerTool
{
bool m_Verbose = false;

public List<ISQLiteFileParser> parsers = new List<ISQLiteFileParser>()
{
new AddressablesBuildLayoutParser(),
new SerializedFileParser(),
};

public int Analyze(
string path,
string databaseName,
Expand All @@ -21,11 +32,19 @@ public int Analyze(
{
m_Verbose = verbose;

using SQLiteWriter writer = new (databaseName, skipReferences);
// TODO: skipReferences needs to be passed into AssetBundleWriter
using SQLiteWriter writer = new (databaseName);

try
{
writer.Begin();
foreach (var parser in parsers)
{
parser.Verbose = verbose;
parser.SkipReferences = skipReferences;
parser.Init(writer.Connection);

}
}
catch (Exception e)
{
Expand All @@ -47,31 +66,54 @@ public int Analyze(
int i = 1;
foreach (var file in files)
{
if (ShouldIgnoreFile(file))
bool foundParser = false;
foreach(var parser in parsers)
{
if (parser.CanParse(file))
{
foundParser = true;
Console.Error.WriteLine(file);
try
{
parser.Parse(file);
ReportProgress(Path.GetRelativePath(path, file), i, files.Length);
countSuccess++;
}
catch (Exception e)
{
EraseProgressLine();
Console.Error.WriteLine();
Console.Error.WriteLine($"Error processing file: {file}");
Console.WriteLine($"{e.GetType()}: {e.Message}");
if (m_Verbose)
Console.WriteLine(e.StackTrace);
countFailures++;
}
++i;
}
}
if (!foundParser)
{
if (m_Verbose)
{
var relativePath = Path.GetRelativePath(path, file);
Console.WriteLine();
Console.WriteLine($"Ignoring {relativePath}");
}
++i;
countIgnored++;
continue;
}
else if (!ProcessFile(file, path, writer, i, files.Length))
{
countFailures++;
}
else
{
countSuccess++;
}
++i;
}

Console.WriteLine();
Console.WriteLine($"Finalizing database. Successfully processed files: {countSuccess}, Failed files: {countFailures}, Ignored files: {countIgnored}");

writer.End();
foreach (var parser in parsers)
{
parser.Dispose();
}

timer.Stop();
Console.WriteLine();
Expand All @@ -80,158 +122,11 @@ public int Analyze(
return 0;
}

bool ShouldIgnoreFile(string file)
{
// Unfortunately there is no standard extension for AssetBundles, and SerializedFiles often have no extension at all.
// Also there is also no distinctive signature at the start of a SerializedFile to immediately recognize it based on its first bytes.
// This makes it difficult to use the "--search-pattern" argument to only pick those files.

// Hence to reduce noise in UnityDataTool output we filter out files that we have a high confidence are
// NOT SerializedFiles or Unity Archives.

string fileName = Path.GetFileName(file);
string extension = Path.GetExtension(file);

return IgnoredFileNames.Contains(fileName) || IgnoredExtensions.Contains(extension);
}

// These lists are based on expected output files in Player, AssetBundle, Addressables and ECS builds.
// However this is by no means exhaustive.
private static readonly HashSet<string> IgnoredFileNames = new()
{
".DS_Store", "boot.config", "archive_dependencies.bin", "scene_info.bin", "app.info", "link.xml",
"catalog.bin", "catalog.hash"
};

private static readonly HashSet<string> IgnoredExtensions = new()
{
".txt", ".resS", ".resource", ".json", ".dll", ".pdb", ".exe", ".manifest", ".entities", ".entityheader",
".ini", ".config"
};

bool ProcessFile(string file, string rootDirectory, SQLiteWriter writer, int fileIndex, int cntFiles)
{
bool successful = true;
try
{
if (IsUnityArchive(file))
{
using (UnityArchive archive = UnityFileSystem.MountArchive(file, "archive:" + Path.DirectorySeparatorChar))
{
if (archive == null)
throw new FileLoadException($"Failed to mount archive: {file}");

try
{
var assetBundleName = Path.GetRelativePath(rootDirectory, file);

writer.BeginAssetBundle(assetBundleName, new FileInfo(file).Length);
ReportProgress(assetBundleName, fileIndex, cntFiles);

foreach (var node in archive.Nodes)
{
if (node.Flags.HasFlag(ArchiveNodeFlags.SerializedFile))
{
try
{
writer.WriteSerializedFile(node.Path, "archive:/" + node.Path, Path.GetDirectoryName(file));
}
catch (Exception e)
{
// the most likely exception here is Microsoft.Data.Sqlite.SqliteException,
// for example 'UNIQUE constraint failed: serialized_files.id'.
// or 'UNIQUE constraint failed: objects.id' which can happen
// if AssetBundles from different builds are being processed by a single call to Analyze
// or if there is a Unity Data Tool bug.
EraseProgressLine();
Console.Error.WriteLine($"Error processing {node.Path} in archive {file}");
Console.Error.WriteLine(e.Message);
Console.WriteLine();

// It is possible some files inside an archive will pass and others will fail, to have a partial analyze.
// Overall that is reported as a failure
successful = false;
}
}
}
}
finally
{
writer.EndAssetBundle();
}
}
}
else
{
// This isn't a Unity Archive file. Try to open it as a SerializedFile.
// Unfortunately there is no standard file extension, or clear signature at the start of the file,
// to test if it truly is a SerializedFile. So this will process files that are clearly not unity build files,
// and there is a chance for crashes and freezes if the parser misinterprets the file content.
var relativePath = Path.GetRelativePath(rootDirectory, file);
writer.WriteSerializedFile(relativePath, file, Path.GetDirectoryName(file));

ReportProgress(relativePath, fileIndex, cntFiles);
}

EraseProgressLine();
}
catch (NotSupportedException)
{
EraseProgressLine();
Console.Error.WriteLine();
//A "failed to load" error will already be logged by the UnityFileSystem library

successful = false;
}
catch (Exception e)
{
EraseProgressLine();
Console.Error.WriteLine();
Console.Error.WriteLine($"Error processing file: {file}");
Console.WriteLine($"{e.GetType()}: {e.Message}");
if (m_Verbose)
Console.WriteLine(e.StackTrace);

successful = false;
}

return successful;
}

private static bool IsUnityArchive(string filePath)
private bool ProcessFile(string file, string path, SQLiteWriter writer, int i, int length)
{
// Check whether a file is a Unity Archive (AssetBundle) by looking for known signatures at the start of the file.
// "UnifyFS" is the current signature, but some older formats of the file are still supported
string[] signatures = { "UnityFS", "UnityWeb", "UnityRaw", "UnityArchive" };
int maxLen = 12; // "UnityArchive".Length
byte[] buffer = new byte[maxLen];

using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
{
int read = fs.Read(buffer, 0, buffer.Length);
foreach (var sig in signatures)
{
if (read >= sig.Length)
{
bool match = true;
for (int i = 0; i < sig.Length; ++i)
{
if (buffer[i] != sig[i])
{
match = false;
break;
}
}
if (match)
return true;
}
}
return false;
}
throw new NotImplementedException();
}



int m_LastProgressMessageLength = 0;

void ReportProgress(string relativePath, int fileIndex, int cntFiles)
Expand Down
Loading