Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion Analyzer/AnalyzerTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public int Analyze(AnalyzeOptions options)
foundParser = true;
try
{
parser.Parse(file);
parser.Parse(file, displayRoot);
ReportProgress(relativePath, i, files.Count);
countSuccess++;
}
Expand Down
2 changes: 1 addition & 1 deletion Analyzer/Resources/Init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ WHERE m.type = 'Material';

INSERT INTO types (id, name) VALUES (-1, 'Scene');

PRAGMA user_version = 7;
PRAGMA user_version = 8;

PRAGMA synchronous = OFF;
PRAGMA journal_mode = MEMORY;
5 changes: 4 additions & 1 deletion Analyzer/SQLite/Handlers/ISQLiteHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ public interface ISQLiteFileParser : IDisposable
{
void Init(SqliteConnection db);
bool CanParse(string filename);
void Parse(string filename);

// rootDirectory is the scanned input path the file was found under; names recorded in the
// database are relative to it, so same-named files in different sub-folders stay distinct.
void Parse(string filename, string rootDirectory);

// Called once after all files have been parsed, so a parser can write data that can only be
// determined from the complete set (e.g. dangling references). No-op for parsers that don't
Expand Down
2 changes: 1 addition & 1 deletion Analyzer/SQLite/Parsers/AddressablesBuildLayoutParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public bool CanParse(string filename)
return false;
}

public void Parse(string filename)
public void Parse(string filename, string rootDirectory)
{
// only init our writer if we are actually parsing a file
m_Writer.Init();
Expand Down
2 changes: 1 addition & 1 deletion Analyzer/SQLite/Parsers/ContentLayoutParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public bool CanParse(string filename)
return IsContentLayoutFile(filename);
}

public void Parse(string filename)
public void Parse(string filename, string rootDirectory)
{
ContentLayout layout;
using (var reader = File.OpenText(filename))
Expand Down
17 changes: 12 additions & 5 deletions Analyzer/SQLite/Parsers/SerializedFileParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ public void Init(SqliteConnection db)
m_SerializedFileIdProvider, m_ContentFileDependencies);
}

public void Parse(string filename)
public void Parse(string filename, string rootDirectory)
{
// only init our writer if we are actually parsing a file
m_Writer.Init();
ProcessFile(filename, Path.GetDirectoryName(filename));
ProcessFile(filename, rootDirectory);
}

bool ShouldIgnoreFile(string file)
Expand Down Expand Up @@ -105,7 +105,12 @@ void ProcessFile(string file, string rootDirectory)

try
{
var archiveName = Path.GetRelativePath(rootDirectory, file);
// Naming an archive by its path relative to the scanned root keeps
// bundles that share a file name in different folders distinct (issue
// #149). Forward slashes make the value platform independent and match
// the name the AssetBundleManifest uses.
var archiveName = Path.GetRelativePath(rootDirectory, file)
.Replace(Path.DirectorySeparatorChar, '/');

m_Writer.BeginArchive(archiveName, new FileInfo(file).Length);

Expand Down Expand Up @@ -173,8 +178,10 @@ void ProcessFile(string file, string rootDirectory)
// This isn't a Unity Archive file, so process it as a SerializedFile.
// Note: The file has already been validated in CanParse() via SerializedFileDetector,
// so we're confident it's a valid SerializedFile at this point.
var relativePath = Path.GetRelativePath(rootDirectory, file);
m_Writer.WriteSerializedFile(relativePath, file, Path.GetDirectoryName(file));
//
// SerializedFiles are recorded with the bare file name, matching how Unity
// references work and requiring uniqueness. (issue #36).
m_Writer.WriteSerializedFile(Path.GetFileName(file), file, Path.GetDirectoryName(file));
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions Analyzer/SQLite/Writers/SerializedFileSQLiteWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ public void EndArchive()
// AssetBundle variants are named "<bundle>.<variant>", and every variant of a bundle contains
// a SerializedFile with the same name. Two archives that differ only in their extension and
// share a SerializedFile are therefore taken to be variants of the same bundle.
//
// The names are paths, and the Path methods below only consider the last segment, so a dot in
// a folder name ("v1.2/main") is not mistaken for a variant suffix.
private static bool LooksLikeAssetBundleVariantPair(string archiveA, string archiveB)
{
if (archiveA == null || archiveB == null || archiveA == archiveB)
Expand Down
9 changes: 8 additions & 1 deletion Documentation/analyzer-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,15 @@ is not inside an archive has no row here.
| Column | Type | Description |
|---|---|---|
| `id` | INTEGER | Analyzer-assigned id. Primary key. |
| `name` | TEXT | The archive's name on the file system. UNIQUE. |
| `name` | TEXT | The archive's path relative to the analyzed directory, with `/` separators. UNIQUE. |
| `file_size` | INTEGER | Size of the archive file in bytes. |

`name` is the path of the archive file relative to the directory that was analyzed, for example
`dlc/weapons/main`. `BuildPipeline.BuildAssetBundles` writes a bundle whose name is a path into a
matching folder structure, so the relative path is both unique and the name the bundle has in the
`AssetBundleManifest`. An archive passed directly on the command line, rather than found by scanning
a directory, is recorded under its bare file name.

`name` is UNIQUE and case-sensitive because analyze supports a single build at a time: two archives
with the same name would make every query ambiguous. A duplicate is detected while writing and
reported as an error; the constraint is the durable backstop. See
Expand Down Expand Up @@ -545,6 +551,7 @@ Any schema change - a new or changed table, view or column - must bump the pragm
| 5 | Added the `dangling_refs` table and view ([#85](https://github.com/Unity-Technologies/UnityDataTools/issues/85)) |
| 6 | `archives.name` is UNIQUE ([#51](https://github.com/Unity-Technologies/UnityDataTools/issues/51)) |
| 7 | Unity 6.6 `build_reports` columns and `build_report_content_*` tables ([#107](https://github.com/Unity-Technologies/UnityDataTools/issues/107)); `asset_name` / `asset_extension` columns on `build_report_source_assets` ([#110](https://github.com/Unity-Technologies/UnityDataTools/issues/110)) |
| 8 | `archives.name` is the path relative to the scanned directory, not the bare file name ([#149](https://github.com/Unity-Technologies/UnityDataTools/issues/149)) |

## Related documentation

Expand Down
2 changes: 1 addition & 1 deletion Documentation/assetbundle-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ be unique within a database, and every variant of a bundle contains a Serialized
name. If the input includes more than one variant of the same bundle, analyze processes the first
one it meets and skips the rest, reporting each skipped file as an AssetBundle variant of the one
that was analyzed (see
[Duplicate SerializedFile name](command-analyze.md#duplicate-serializedfile-name--duplicate-archive-name)).
[Duplicate SerializedFile name](command-analyze.md#duplicate-serializedfile-name)).
The resulting database is still valid; it simply describes one variant. To choose which, pass only
that variant of each bundle, for example only the `.hd` files together with the non-variant bundles.
To compare variants, analyze each into its own database as described in
Expand Down
26 changes: 16 additions & 10 deletions Documentation/command-analyze.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,28 +179,34 @@ This error occurs when SerializedFiles are built without TypeTrees. The command
UnityDataTool analyze /path/to/bundles --typetree-data /path/to/typetree.bin
```

### Duplicate SerializedFile name / Duplicate archive name
### Duplicate SerializedFile name

```
Skipping build2\level0: Duplicate SerializedFile name 'level0'. Only a single build can be analyzed at a time; the same SerializedFile name cannot be analyzed twice.
```
or
```
Skipping build2\assetbundle: Duplicate archive name 'assetbundle'. Each analyzed archive must have a unique name; only a single build can be analyzed at a time.
```
or
```
Skipping ui.sd: AssetBundle variant of 'ui.hd', which was already analyzed (both contain SerializedFile 'CAB-5d40f7cad7c871cf2ad2af19ac542994'). Only one variant of each bundle can be analyzed.
```

**analyze only supports a single build at a time.** Unity resolves references between SerializedFiles
by file name, so two files that share a name are indistinguishable to those references — there is no
way to tell which copy a reference points at. For that reason each SerializedFile name (and each
archive name) may appear only once in a database.
way to tell which copy a reference points at. For that reason each SerializedFile name may appear
only once in a database.

Archive names do not have to be unique across folders: an archive is recorded under its path
relative to the analyzed directory, so two bundles that share a file name in different sub-folders
(`dlc/weapons/main` and `dlc/armor/main`) are both analyzed. Two builds analyzed together still
collide, both on their SerializedFiles and, when each build is passed as its own input path, on
their archive names:

```
Skipping build2\assetbundle: Duplicate archive name 'assetbundle'. Each analyzed archive must have a unique name; only a single build can be analyzed at a time.
```

When analyze encounters a second file or archive with a name it has already processed, it prints one
of the messages above, **skips that file or archive** (counting it as a failed file), and continues
with the rest of the input. The already-analyzed copy is kept; the duplicate's content is ignored.
When analyze encounters a second file or archive with a name it has already processed, it **skips
that file or archive** (counting it as a failed file) and continues with the rest of the input.
The already-analyzed copy is kept; the duplicate's content is ignored.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good.

I think this example error above should be removed:

Skipping build2\assetbundle: Duplicate archive name 'assetbundle'. Each analyzed archive must have a unique name; only a single build can be analyzed at a time.

And

When analyze encounters a second file or archive with a name it has already processed, it prints one
of the messages above

It seems this section needs more revision to make sure it is accurate (duplicate archives are ok, but serialized files need to be unique. Hence it is still true that we cannot analyze two versions of the same build and AssetBundle variants trigger an error.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AGENT: Reworked the section. Renamed it to "Duplicate SerializedFile name", led with SerializedFile uniqueness as the real constraint, and dropped the claim that archive names must be unique.

I kept the Duplicate archive name example, but moved it into a paragraph that says when it now happens: archive names no longer have to be unique across folders, but two builds analyzed together still collide, and when each build is passed as its own input path the archive names collide too (each path is its own root, so both bundles are still named assetbundle). That case is still reachable and still covered by a test, so removing the message entirely would leave an error text that appears in output but nowhere in the docs. Happy to drop it if you'd rather the section only covered SerializedFiles.

This is expected when the input contains more than one build, and in these common cases:

Expand Down
81 changes: 81 additions & 0 deletions UnityDataTool.Tests/AnalyzeDuplicateNameTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ namespace UnityDataTools.UnityDataTool.Tests;
// single-line message instead of a raw "UNIQUE constraint failed" SQLite error. Covers the three
// scenarios from the issue: loose files, archives with the same name, and differently-named
// archives (hashed bundle names) that share the same inner SerializedFile.
//
// Also covers the opposite case (issue #149): bundles sharing a file name in different sub-folders
// of one scanned directory are distinct archives, not duplicates.
public class AnalyzeDuplicateNameTests
{
private string m_TestOutputFolder;
Expand Down Expand Up @@ -159,4 +162,82 @@ public async Task Analyze_DifferentArchiveNamesSharingSerializedFile_SkippedWith
WHERE name LIKE 'CAB-%' AND id IN (SELECT serialized_file FROM objects)",
1, "the shared inner SerializedFile should be analyzed only once");
}

// Issue #149: an AssetBundle name can be a path, so a build commonly contains several bundles
// with the same file name in different folders. They are recorded under their path relative to
// the scanned directory and are not treated as duplicates.
[Test]
public async Task Analyze_SameFileNameInDifferentFolders_NamedByRelativePath()
{
var sourceFolder = Path.Combine(m_AssetBundlesFolder, "2019.4.0f1");
var folderA = Path.Combine(m_TestOutputFolder, "dlc", "weapons");
var folderB = Path.Combine(m_TestOutputFolder, "dlc", "armor");
Directory.CreateDirectory(folderA);
Directory.CreateDirectory(folderB);
File.Copy(Path.Combine(sourceFolder, "assetbundle"), Path.Combine(folderA, "main"));
File.Copy(Path.Combine(sourceFolder, "scenes"), Path.Combine(folderB, "main"));
var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder);

var (exitCode, stderr) = await RunAnalyze(m_TestOutputFolder, "-o", databasePath);

Assert.AreEqual(0, exitCode);
StringAssert.DoesNotContain("Duplicate archive name", stderr);
StringAssert.DoesNotContain("UNIQUE constraint", stderr);

using var db = SQLTestHelper.OpenDatabase(databasePath);
SQLTestHelper.AssertQueryInt(db,
"SELECT COUNT(*) FROM archives WHERE name IN ('dlc/weapons/main', 'dlc/armor/main')",
2, "both bundles should be recorded under their relative path");
Assert.Greater(SQLTestHelper.QueryInt(db,
"SELECT COUNT(*) FROM object_view WHERE archive = 'dlc/weapons/main'"), 0);
Assert.Greater(SQLTestHelper.QueryInt(db,
"SELECT COUNT(*) FROM object_view WHERE archive = 'dlc/armor/main'"), 0);
}

// A file named directly on the command line has no scanned directory to be relative to, so it
// keeps its bare file name.
[Test]
public async Task Analyze_FileNamedDirectly_KeepsBareFileName()
{
var source = Path.Combine(m_AssetBundlesFolder, "2019.4.0f1", "assetbundle");
var nested = Path.Combine(m_TestOutputFolder, "dlc", "weapons");
Directory.CreateDirectory(nested);
var bundle = Path.Combine(nested, "main");
File.Copy(source, bundle);
var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder);

var (exitCode, _) = await RunAnalyze(bundle, "-o", databasePath);

Assert.AreEqual(0, exitCode);

using var db = SQLTestHelper.OpenDatabase(databasePath);
SQLTestHelper.AssertQueryString(db, "SELECT name FROM archives", "main",
"a directly-named archive keeps its bare file name");
}

// The variant check compares the extensions of two archive names, which are now paths. A dot
// in a folder name is not an extension, so two bundles under "v1.2" and "v1.3" must be
// reported as a plain duplicate rather than as variants of each other.
[Test]
public async Task Analyze_DottedFolderNames_NotReportedAsVariants()
{
var source = Path.Combine(m_AssetBundlesFolder, "2019.4.0f1", "assetbundle");
foreach (var folder in new[] { "v1.2", "v1.3" })
{
Directory.CreateDirectory(Path.Combine(m_TestOutputFolder, folder));
File.Copy(source, Path.Combine(m_TestOutputFolder, folder, "main"));
}
var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder);

var (exitCode, stderr) = await RunAnalyze(m_TestOutputFolder, "-o", databasePath);

Assert.AreEqual(0, exitCode);
StringAssert.Contains("Duplicate SerializedFile name", stderr);
StringAssert.DoesNotContain("AssetBundle variant", stderr);

using var db = SQLTestHelper.OpenDatabase(databasePath);
SQLTestHelper.AssertQueryInt(db,
"SELECT COUNT(*) FROM archives WHERE name IN ('v1.2/main', 'v1.3/main')",
2, "both archives should be recorded under their dotted-folder relative path");
}
}
Loading