Skip to content

Commit 1abf5fd

Browse files
[#49] Allow analyze to accept multiple file or directory paths
The analyze command now takes a variadic positional argument: each path may be a file or a directory. Directories are scanned (honoring --search-pattern/--no-recurse); explicitly named files are analyzed directly. This makes analyzing a single file simple and lets files from multiple locations (e.g. a build output directory plus an external build report) go into one database in a single invocation. - AnalyzerTool.AnalyzeOptions.Paths replaces the single Path; CollectFiles expands inputs, dedups, and tracks per-input roots for relative display. - Convert tests that used the awkward ". -p <file>" form to pass paths directly; add coverage for the directory-plus-external-file case. - Document the new usage in command-analyze.md, analyzer.md and buildreport.md (including the Unity 6.6 build history workflow). Fixes #49
1 parent 2e659d4 commit 1abf5fd

8 files changed

Lines changed: 155 additions & 37 deletions

File tree

Analyzer/AnalyzerTool.cs

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ public class AnalyzerTool
2323

2424
public class AnalyzeOptions
2525
{
26-
public string Path { get; init; }
26+
// Each entry is a file or a directory. Directories are scanned using SearchPattern and
27+
// NoRecursion; files are always included regardless of SearchPattern.
28+
public IReadOnlyList<string> Paths { get; init; }
2729
public string DatabaseName { get; init; }
2830
public string SearchPattern { get; init; } = "*";
2931
public bool SkipReferences { get; init; }
@@ -59,17 +61,15 @@ public int Analyze(AnalyzeOptions options)
5961
var timer = new Stopwatch();
6062
timer.Start();
6163

62-
var files = Directory.GetFiles(
63-
m_Options.Path,
64-
m_Options.SearchPattern,
65-
m_Options.NoRecursion ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories);
64+
var files = CollectFiles();
6665

6766
int countFailures = 0;
6867
int countSuccess = 0;
6968
int countIgnored = 0;
7069
int i = 1;
71-
foreach (var file in files)
70+
foreach (var (file, displayRoot) in files)
7271
{
72+
var relativePath = Path.GetRelativePath(displayRoot, file);
7373
bool foundParser = false;
7474
foreach (var parser in parsers)
7575
{
@@ -79,15 +79,14 @@ public int Analyze(AnalyzeOptions options)
7979
try
8080
{
8181
parser.Parse(file);
82-
ReportProgress(Path.GetRelativePath(m_Options.Path, file), i, files.Length);
82+
ReportProgress(relativePath, i, files.Count);
8383
countSuccess++;
8484
}
8585
catch (SerializedFileOpenException e)
8686
{
8787
// Expected failure — the file content could not be parsed.
8888
// Don't print a stack trace; it adds no value for this known failure mode.
8989
EraseProgressLine();
90-
var relativePath = Path.GetRelativePath(m_Options.Path, file);
9190
Console.Error.WriteLine($"Failed to open: {relativePath}");
9291
var hint = SerializedFileDetector.GetOpenFailureHint(e.FilePath);
9392
if (hint != null)
@@ -98,7 +97,6 @@ public int Analyze(AnalyzeOptions options)
9897
{
9998
// Unexpected failure (SQL error, I/O error, bug, etc.) — print full details.
10099
EraseProgressLine();
101-
var relativePath = Path.GetRelativePath(m_Options.Path, file);
102100
Console.Error.WriteLine($"Failed to process: {relativePath}");
103101
if (m_Options.Verbose)
104102
{
@@ -115,7 +113,6 @@ public int Analyze(AnalyzeOptions options)
115113
{
116114
if (m_Options.Verbose)
117115
{
118-
var relativePath = Path.GetRelativePath(m_Options.Path, file);
119116
Console.WriteLine();
120117
Console.WriteLine($"Ignoring {relativePath}");
121118
}
@@ -141,6 +138,40 @@ public int Analyze(AnalyzeOptions options)
141138
return 0;
142139
}
143140

141+
// Expands the input paths into the concrete files to analyze. Each result pairs the file with the
142+
// root used to render its relative path in progress/error messages: the scanned directory for files
143+
// found by scanning, or the file's own directory for explicitly-named files. Duplicates reached via
144+
// more than one input are analyzed once.
145+
List<(string FullPath, string DisplayRoot)> CollectFiles()
146+
{
147+
var searchOption = m_Options.NoRecursion ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories;
148+
var collected = new List<(string FullPath, string DisplayRoot)>();
149+
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
150+
151+
foreach (var inputPath in m_Options.Paths)
152+
{
153+
if (Directory.Exists(inputPath))
154+
{
155+
foreach (var file in Directory.GetFiles(inputPath, m_Options.SearchPattern, searchOption))
156+
{
157+
if (seen.Add(Path.GetFullPath(file)))
158+
collected.Add((file, inputPath));
159+
}
160+
}
161+
else if (File.Exists(inputPath))
162+
{
163+
if (seen.Add(Path.GetFullPath(inputPath)))
164+
collected.Add((inputPath, Path.GetDirectoryName(Path.GetFullPath(inputPath))));
165+
}
166+
else
167+
{
168+
Console.Error.WriteLine($"Warning: path not found, skipping: {inputPath}");
169+
}
170+
}
171+
172+
return collected;
173+
}
174+
144175
int m_LastProgressMessageLength = 0;
145176

146177
void ReportProgress(string relativePath, int fileIndex, int cntFiles)

Documentation/analyzer.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,10 @@ The [AnalyzerTool](../Analyzer/AnalyzerTool.cs) class is the API entry point. Th
188188
Analyze. It is currently hard coded to write using the [SQLiteWriter](../Analyzer/SQLite/SQLiteWriter.cs),
189189
but this approach could be extended to add support for other outputs.
190190

191-
Calling this method will recursively process the files matching the search pattern in the provided
192-
path. It will add a row in the 'objects' table for each serialized object. This table contain basic
193-
information such as the size and the name of the object (if it has one).
191+
Calling this method processes the provided paths, which can be individual files or directories.
192+
Directories are scanned recursively for files matching the search pattern (unless recursion is
193+
disabled). It will add a row in the 'objects' table for each serialized object. This table contain
194+
basic information such as the size and the name of the object (if it has one).
194195

195196
## Extending the Library
196197

Documentation/buildreport.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,13 @@ SELECT build_time_asset_path from build_report_source_assets WHERE build_time_as
4949

5050
## Cross-Referencing with Build Output
5151

52-
For comprehensive analysis, run `analyze` on both the build output **and** the matching build report file. Use a clean build to ensure PackedAssets information is fully populated. You may need to copy the build report into the build output directory so both are found by `analyze`.
52+
For comprehensive analysis, run `analyze` on both the build output **and** the matching build report file. Use a clean build to ensure PackedAssets information is fully populated.
53+
54+
`analyze` accepts multiple path arguments, each of which can be a file or a directory, so you can pass the build output directory together with the build report path (or the directory containing it) in a single command:
55+
56+
```bash
57+
UnityDataTool analyze /path/to/build/output /path/to/Library/LastBuild.buildreport
58+
```
5359

5460
PackedAssets data provides source asset information for each object that isn't available when analyzing only the build output. Objects are listed in the same order as they appear in the output SerializedFile, .resS, or .resource file.
5561

@@ -64,11 +70,33 @@ PackedAssets data provides source asset information for each object that isn't a
6470

6571
## Working with Multiple Build Reports
6672

67-
Multiple build reports can be imported into the same database if their filenames differ. This enables:
73+
Multiple build reports can be imported into the same database if their filenames differ. Pass each report (and any build output directories) as separate path arguments to a single `analyze` command. This enables:
6874
- Comprehensive build history tracking
6975
- Cross-build comparisons
7076
- Identifying duplicated data between Player and AssetBundle builds
7177

78+
### Prior to Unity 6.6
79+
80+
Each build overwrites `Library/LastBuild.buildreport`. To compare builds, manually collect the report after each build, rename the copies so the filenames are unique (the analyzer keys serialized files by filename), then pass them to `analyze`:
81+
82+
```bash
83+
UnityDataTool analyze build1.buildreport build2.buildreport
84+
```
85+
86+
### Unity 6.6 and later
87+
88+
Player and content directory builds record a structured [build history](https://docs.unity3d.com/6000.6/Documentation/ScriptReference/Build.BuildHistory.html) (default location `Library/BuildHistory`). Unity assigns each build its own directory and gives every build report a unique GUID-based filename, so there is no need to copy or rename reports to compare them. Run `analyze` on the entire build history folder, or on specific build report directories:
89+
90+
```bash
91+
# Analyze every build in the history
92+
UnityDataTool analyze Library/BuildHistory
93+
94+
# Analyze two specific builds
95+
UnityDataTool analyze Library/BuildHistory/20260504-153912Z-2dd7642e Library/BuildHistory/20260504-153855Z-7aff42f4
96+
```
97+
98+
AssetBundle builds are not tracked in the build history; they still write only to `Library/LastBuild.buildreport`.
99+
72100
See the schema sections below for guidance on writing queries that handle multiple build reports correctly.
73101

74102
## Alternatives

Documentation/command-analyze.md

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,41 @@ The `analyze` command extracts information from Unity Archives (e.g. AssetBundle
55
## Quick Reference
66

77
```
8-
UnityDataTool analyze <path> [options]
8+
UnityDataTool analyze <paths>... [options]
99
```
1010

1111
| Option | Description | Default |
1212
|--------|-------------|---------|
13-
| `<path>` | Path to folder containing files to analyze | *(required)* |
13+
| `<paths>...` | One or more files or directories to analyze. Directories are scanned; files are analyzed directly. | *(required)* |
1414
| `-o, --output-file <file>` | Output database filename | `database.db` |
15-
| `-p, --search-pattern <pattern>` | File search pattern (`*` and `?` supported) | `*` |
15+
| `-p, --search-pattern <pattern>` | File search pattern applied when scanning directories (`*` and `?` supported) | `*` |
1616
| `-s, --skip-references` | Do not extract references (smaller DB, no `refs` table). CRC is still computed. | `false` |
1717
| `--skip-crc` | Skip the CRC32 checksum calculation (faster; `objects.crc32` will be 0) | `false` |
1818
| `-v, --verbose` | Show more information during analysis | `false` |
19-
| `--no-recurse` | Do not recurse into sub-directories | `false` |
19+
| `--no-recurse` | Do not recurse into sub-directories when scanning directories | `false` |
2020
| `-d, --typetree-data <file>` | Load an external TypeTree data file before processing (Unity 6.5+) ||
2121

22+
There is no way to append to an existing database, so every file you want in the results must be
23+
included in a single `analyze` invocation. Pass multiple paths to combine files from more than one
24+
location into the same database.
25+
2226
## Examples
2327

2428
Analyze all files in a directory:
2529
```bash
2630
UnityDataTool analyze /path/to/asset/bundles
2731
```
2832

33+
Analyze a single file (no need for `.` plus `-p`):
34+
```bash
35+
UnityDataTool analyze /path/to/asset/bundles/my.bundle
36+
```
37+
38+
Combine a build output directory with a build report file kept in a separate location:
39+
```bash
40+
UnityDataTool analyze /path/to/build/output /path/to/Library/LastBuild.buildreport
41+
```
42+
2943
Analyze only `.bundle` files and specify a custom database name:
3044
```bash
3145
UnityDataTool analyze /path/to/asset/bundles -o my_database.db -p "*.bundle"
@@ -42,7 +56,9 @@ See also [Analyze Examples](../../Documentation/analyze-examples.md).
4256

4357
## What Can Be Analyzed
4458

45-
The analyze command works with the following types of directories:
59+
Each path may be an individual file or a directory. Directories are scanned (honoring
60+
`--search-pattern` and `--no-recurse`); individually-named files are always analyzed. The analyze
61+
command works with the following types of input:
4662

4763
| Input Type | Description |
4864
|------------|-------------|

UnityDataTool.Tests/BuildReportTests.cs

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public async Task Analyze_BuildReport_ContainsExpected_ObjectInfo(
4646
{
4747
var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder);
4848

49-
var args = new List<string> { "analyze", m_TestDataFolder, "-p", "AssetBundle.buildreport" };
49+
var args = new List<string> { "analyze", Path.Combine(m_TestDataFolder, "AssetBundle.buildreport") };
5050
if (skipReferences)
5151
args.Add("--skip-references");
5252

@@ -159,7 +159,7 @@ public async Task Analyze_BuildReport_ContainsExpectedReferences(
159159
{
160160
var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder);
161161

162-
var args = new List<string> { "analyze", m_TestDataFolder, "-p", "AssetBundle.buildreport" };
162+
var args = new List<string> { "analyze", Path.Combine(m_TestDataFolder, "AssetBundle.buildreport") };
163163
if (skipReferences)
164164
args.Add("--skip-references");
165165

@@ -214,7 +214,7 @@ public async Task Analyze_BuildReport_AssetBundle_ContainsBuildReportData()
214214
{
215215
var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder);
216216

217-
var args = new List<string> { "analyze", m_TestDataFolder, "-p", "AssetBundle.buildreport" };
217+
var args = new List<string> { "analyze", Path.Combine(m_TestDataFolder, "AssetBundle.buildreport") };
218218

219219
Assert.AreEqual(0, await Program.Main(args.ToArray()));
220220
using var db = SQLTestHelper.OpenDatabase(databasePath);
@@ -246,7 +246,7 @@ public async Task Analyze_BuildReport_Player_ContainsBuildReportData()
246246
{
247247
var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder);
248248

249-
var args = new List<string> { "analyze", m_TestDataFolder, "-p", "Player.buildreport" };
249+
var args = new List<string> { "analyze", Path.Combine(m_TestDataFolder, "Player.buildreport") };
250250

251251
Assert.AreEqual(0, await Program.Main(args.ToArray()));
252252
using var db = SQLTestHelper.OpenDatabase(databasePath);
@@ -284,7 +284,7 @@ public async Task Analyze_BuildReport_AssetBundle_ContainsPackedAssetsData()
284284
{
285285
var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder);
286286

287-
var args = new List<string> { "analyze", m_TestDataFolder, "-p", "AssetBundle.buildreport" };
287+
var args = new List<string> { "analyze", Path.Combine(m_TestDataFolder, "AssetBundle.buildreport") };
288288

289289
Assert.AreEqual(0, await Program.Main(args.ToArray()));
290290
using var db = SQLTestHelper.OpenDatabase(databasePath);
@@ -344,13 +344,49 @@ public async Task Analyze_BuildReport_AssetBundle_ContainsPackedAssetsData()
344344
"Unexpected path in build_report_packed_assets_view");
345345
}
346346

347+
// The motivating case for issue #49: combine a scanned build-output directory with a build
348+
// report file that lives in a separate location, all in a single analyze invocation.
349+
[Test]
350+
public async Task Analyze_DirectoryPlusExternalFile_BothIncluded()
351+
{
352+
var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder);
353+
354+
// Simulate a build output directory containing one report, with a second report kept elsewhere.
355+
var buildOutputDir = Path.Combine(m_TestOutputFolder, "build_output");
356+
Directory.CreateDirectory(buildOutputDir);
357+
File.Copy(Path.Combine(m_TestDataFolder, "AssetBundle.buildreport"),
358+
Path.Combine(buildOutputDir, "AssetBundle.buildreport"));
359+
360+
var args = new string[]
361+
{
362+
"analyze",
363+
buildOutputDir,
364+
Path.Combine(m_TestDataFolder, "Player.buildreport"),
365+
};
366+
367+
Assert.AreEqual(0, await Program.Main(args));
368+
using var db = SQLTestHelper.OpenDatabase(databasePath);
369+
370+
SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM build_reports", 2,
371+
"Expected both the scanned directory's report and the external report");
372+
SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM build_reports WHERE build_type = 'AssetBundle'", 1,
373+
"Expected the AssetBundle report from the scanned directory");
374+
SQLTestHelper.AssertQueryInt(db, "SELECT COUNT(*) FROM build_reports WHERE build_type = 'Player'", 1,
375+
"Expected the Player report passed as an external file");
376+
}
377+
347378
[Test]
348379
public async Task Analyze_BuildReports_BothReports_ContainsBuildReportFilesData()
349380
{
350381
var databasePath = SQLTestHelper.GetDatabasePath(m_TestOutputFolder);
351382

352-
// Analyze multiple BuildReports into the same database
353-
var args = new List<string> { "analyze", m_TestDataFolder, "-p", "*.buildreport" };
383+
// Analyze multiple BuildReports into the same database by passing each file explicitly.
384+
var args = new List<string>
385+
{
386+
"analyze",
387+
Path.Combine(m_TestDataFolder, "AssetBundle.buildreport"),
388+
Path.Combine(m_TestDataFolder, "Player.buildreport"),
389+
};
354390

355391
Assert.AreEqual(0, await Program.Main(args.ToArray()));
356392
using var db = SQLTestHelper.OpenDatabase(databasePath);

UnityDataTool.Tests/SerializedFileCommandTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -605,8 +605,8 @@ public async Task ObjectList_CrossValidate_MatchesAnalyzeCommand()
605605
{
606606
// First, run analyze command to create database
607607
var databasePath = Path.Combine(m_TestOutputFolder, "test_analyze.db");
608-
var analyzePath = m_TestDataFolder;
609-
Assert.AreEqual(0, await Program.Main(new string[] { "analyze", analyzePath, "-o", databasePath, "-p", "level0" }));
608+
var analyzePath = Path.Combine(m_TestDataFolder, "level0");
609+
Assert.AreEqual(0, await Program.Main(new string[] { "analyze", analyzePath, "-o", databasePath }));
610610

611611
// Now run serialized-file objectlist
612612
var path = Path.Combine(m_TestDataFolder, "level0");

UnityDataTool.Tests/UnityDataToolPlayerDataTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public async Task Analyze_PlayerDataNoTypeTree_ReportsFailureCorrectly()
102102
Console.SetError(swErr);
103103

104104
// Analyze should return 0 even if files fail (non-zero would be a critical error)
105-
Assert.AreEqual(0, await Program.Main(new string[] { "analyze", testDataFolder, "-p", "level0" }));
105+
Assert.AreEqual(0, await Program.Main(new string[] { "analyze", Path.Combine(testDataFolder, "level0") }));
106106

107107
var output = swOut.ToString() + swErr.ToString();
108108

0 commit comments

Comments
 (0)