├── img
├── hh_main.png
├── hh_report_main.png
├── hh_report_errors.png
├── hh_report_includes.png
├── hh_report_missing.png
├── hh_godot_win.json
└── hh_blender_win.json
├── header_hero
├── AppIcon.ico
├── App.axaml
├── App.axaml.cs
├── ProgressDialog.axaml
├── MessageBox3.axaml
├── Program.cs
├── header_hero.sln
├── Data
│ ├── SourceFile.cs
│ └── Project.cs
├── app.manifest
├── ProgressDialog.axaml.cs
├── MessageBox3.axaml.cs
├── header_hero.csproj
├── Utils
│ ├── AppSettings.cs
│ └── SystemIncludesLocator.cs
├── Parser
│ ├── Analytics.cs
│ ├── Parser.cs
│ ├── Report.cs
│ └── Scanner.cs
├── MainWindow.axaml
├── ReportWindow.axaml.cs
├── MainWindow.axaml.cs
└── ReportWindow.axaml
├── .gitignore
├── .github
└── workflows
│ └── build.yml
└── readme.md
/img/hh_main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aras-p/header_hero/HEAD/img/hh_main.png
--------------------------------------------------------------------------------
/img/hh_report_main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aras-p/header_hero/HEAD/img/hh_report_main.png
--------------------------------------------------------------------------------
/header_hero/AppIcon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aras-p/header_hero/HEAD/header_hero/AppIcon.ico
--------------------------------------------------------------------------------
/img/hh_report_errors.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aras-p/header_hero/HEAD/img/hh_report_errors.png
--------------------------------------------------------------------------------
/img/hh_report_includes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aras-p/header_hero/HEAD/img/hh_report_includes.png
--------------------------------------------------------------------------------
/img/hh_report_missing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aras-p/header_hero/HEAD/img/hh_report_missing.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | obj
2 | bin
3 | .vs
4 | *.exe
5 | *.user
6 | *.suo
7 | *.DS_Store
8 | *.pidb
9 | *.userprefs
10 | .idea
11 |
--------------------------------------------------------------------------------
/header_hero/App.axaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/img/hh_godot_win.json:
--------------------------------------------------------------------------------
1 | {
2 | "ProjectRoot": "C:\\code\\projects\\other\\godot",
3 | "ScanDirectories": [
4 | "core",
5 | "drivers",
6 | "editor",
7 | "main",
8 | "modules",
9 | "platform",
10 | "scene",
11 | "servers"
12 | ],
13 | "IncludeDirectories": [
14 | ".",
15 | "thirdparty\\graphite\\src",
16 | "thirdparty\\jolt_physics",
17 | "thirdparty\\mbedtls\\include"
18 | ],
19 | "PrecompiledHeader": ""
20 | }
--------------------------------------------------------------------------------
/header_hero/App.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls.ApplicationLifetimes;
3 | using Avalonia.Markup.Xaml;
4 |
5 | namespace HeaderHero;
6 |
7 | public partial class App : Application
8 | {
9 | public override void Initialize()
10 | {
11 | AvaloniaXamlLoader.Load(this);
12 | }
13 |
14 | public override void OnFrameworkInitializationCompleted()
15 | {
16 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
17 | {
18 | desktop.MainWindow = new MainWindow();
19 | }
20 |
21 | base.OnFrameworkInitializationCompleted();
22 | }
23 | }
--------------------------------------------------------------------------------
/header_hero/ProgressDialog.axaml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/header_hero/MessageBox3.axaml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/header_hero/Program.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using System;
3 |
4 | namespace HeaderHero;
5 |
6 | class Program
7 | {
8 | // Initialization code. Don't use any Avalonia, third-party APIs or any
9 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
10 | // yet and stuff might break.
11 | [STAThread]
12 | public static void Main(string[] args) => BuildAvaloniaApp()
13 | .StartWithClassicDesktopLifetime(args);
14 |
15 | // Avalonia configuration, don't remove; also used by visual designer.
16 | public static AppBuilder BuildAvaloniaApp()
17 | => AppBuilder.Configure()
18 | .UsePlatformDetect()
19 | .WithInterFont()
20 | .LogToTrace();
21 | }
--------------------------------------------------------------------------------
/header_hero/header_hero.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "header_hero", "header_hero.csproj", "{F0122516-3BC9-48B2-BEE5-4F7D03E0F52A}"
4 | EndProject
5 | Global
6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
7 | Debug|Any CPU = Debug|Any CPU
8 | Release|Any CPU = Release|Any CPU
9 | EndGlobalSection
10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
11 | {F0122516-3BC9-48B2-BEE5-4F7D03E0F52A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
12 | {F0122516-3BC9-48B2-BEE5-4F7D03E0F52A}.Debug|Any CPU.Build.0 = Debug|Any CPU
13 | {F0122516-3BC9-48B2-BEE5-4F7D03E0F52A}.Release|Any CPU.ActiveCfg = Release|Any CPU
14 | {F0122516-3BC9-48B2-BEE5-4F7D03E0F52A}.Release|Any CPU.Build.0 = Release|Any CPU
15 | EndGlobalSection
16 | EndGlobal
17 |
--------------------------------------------------------------------------------
/header_hero/Data/SourceFile.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace HeaderHero.Data;
4 |
5 | public class SourceFile
6 | {
7 | public List LocalIncludes { get; init; } = [];
8 | public List SystemIncludes { get; init; } = [];
9 | public List AbsoluteIncludes { get; set; } = [];
10 | public int Lines { get; init; }
11 | public bool Precompiled { get; init; }
12 |
13 | public static bool IsTranslationUnitExtension(string ext)
14 | {
15 | return ext is ".cpp" or ".c" or ".cc" or ".cxx" or ".mm" or ".m";
16 | }
17 | public static bool IsHeaderExtension(string ext)
18 | {
19 | return ext is ".h" or ".hh" or ".hpp" or ".hxx" or "" or ".";
20 | }
21 |
22 | public static bool IsTranslationUnitPath(string path)
23 | {
24 | return IsTranslationUnitExtension(System.IO.Path.GetExtension(path));
25 | }
26 | }
--------------------------------------------------------------------------------
/header_hero/app.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 |
2 | name: Build Header Hero
3 |
4 | on:
5 | push:
6 | branches: [ "main" ]
7 | pull_request:
8 | branches: [ "main" ]
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | strategy:
14 | fail-fast: true
15 | matrix:
16 | rid: [win-x64, osx-arm64, linux-x64]
17 |
18 | steps:
19 | - name: Checkout
20 | uses: actions/checkout@v4
21 |
22 | - name: Install .NET
23 | uses: actions/setup-dotnet@v4
24 | with:
25 | dotnet-version: 9.0.x
26 |
27 | - name: Restore
28 | working-directory: header_hero
29 | run: dotnet restore
30 |
31 | - name: Publish ${{ matrix.rid }}
32 | working-directory: header_hero
33 | run: |
34 | dotnet publish \
35 | -c Release \
36 | -r ${{ matrix.rid }} \
37 | --self-contained true \
38 | /p:PublishSingleFile=true \
39 | /p:PublishTrimmed=true \
40 | /p:TrimMode=link \
41 | -o ../out/${{ matrix.rid }}
42 |
43 | - name: Upload artifact
44 | uses: actions/upload-artifact@v4
45 | with:
46 | name: HeaderHero-${{ matrix.rid }}.zip
47 | path: out/${{ matrix.rid }}
48 |
--------------------------------------------------------------------------------
/header_hero/ProgressDialog.axaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Avalonia.Controls;
4 | using Avalonia.Threading;
5 | using HeaderHero.Parser;
6 |
7 | namespace HeaderHero;
8 |
9 | public partial class ProgressDialog : Window
10 | {
11 | readonly ProgressFeedback _feedback = new();
12 | Func _work;
13 | public ProgressDialog()
14 | {
15 | InitializeComponent();
16 | }
17 |
18 | public void Start(Func work)
19 | {
20 | _work = work;
21 | Opened += async (_, _) =>
22 | {
23 | await RunWorkAsync();
24 | };
25 | }
26 |
27 | async Task RunWorkAsync()
28 | {
29 | try
30 | {
31 | await Task.Run(() => _work!(_feedback));
32 | }
33 | catch (Exception ex)
34 | {
35 | await Dispatcher.UIThread.InvokeAsync(() =>
36 | {
37 | Console.WriteLine(ex);
38 | });
39 | }
40 |
41 | // Close the dialog when work is done
42 | await Dispatcher.UIThread.InvokeAsync(Close);
43 | }
44 |
45 | public void Poll()
46 | {
47 | ProgressBar.Maximum = Math.Max(1, _feedback.Count);
48 | ProgressBar.Value = Math.Clamp(_feedback.Item, 0, ProgressBar.Maximum);
49 |
50 | ProgressReportLabel.Text = $"{_feedback.Item}/{_feedback.Count}";
51 | MessageLabel.Text = _feedback.Message;
52 | Title = _feedback.Title;
53 | }
54 | }
--------------------------------------------------------------------------------
/header_hero/MessageBox3.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Controls;
2 | using Avalonia.Input;
3 |
4 | namespace HeaderHero;
5 |
6 | public partial class MessageBox3 : Window
7 | {
8 | readonly int cancelIndex;
9 | public MessageBox3(string title, string message, params string[] buttons)
10 | {
11 | InitializeComponent();
12 | Opened += (_, _) =>
13 | {
14 | // otherwise keyboard navigation sometimes stays in the parent window
15 | if (Owner != null) Owner.IsEnabled = false;
16 | };
17 |
18 | Title = title;
19 | MessageText.Text = message;
20 |
21 | cancelIndex = buttons.Length - 1;
22 |
23 | for (int i = 0; i < buttons.Length; i++)
24 | {
25 | int index = i;
26 | var btn = new Button
27 | {
28 | Content = buttons[i],
29 | MinWidth = 60
30 | };
31 | btn.Click += (_, _) => Close(index);
32 | if (index == 0)
33 | btn.IsDefault = true;
34 | ButtonsPanel.Children.Add(btn);
35 | }
36 |
37 | Closing += OnClosing;
38 | KeyDown += OnKeyDown;
39 | }
40 |
41 | void OnClosing(object sender, WindowClosingEventArgs e)
42 | {
43 | if (Owner != null) Owner.IsEnabled = true;
44 | if (!e.IsProgrammatic)
45 | Close(cancelIndex);
46 | }
47 |
48 | void OnKeyDown(object sender, KeyEventArgs e)
49 | {
50 | if (e.Key == Key.Escape)
51 | {
52 | if (Owner != null) Owner.IsEnabled = true;
53 | Close(cancelIndex);
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/header_hero/header_hero.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | WinExe
4 | net9.0
5 | disable
6 | app.manifest
7 | true
8 | HeaderHero
9 | HeaderHero
10 | AppIcon.ico
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | None
23 | All
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/header_hero/Utils/AppSettings.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Text.Json;
4 |
5 | namespace HeaderHero.Utils;
6 |
7 | public sealed class AppSettings
8 | {
9 | public string LastProject { get; set; }
10 |
11 | static readonly Lazy _instance = new(Load);
12 | public static AppSettings Instance => _instance.Value;
13 |
14 | static string SettingsPath =>
15 | Path.Combine(
16 | Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
17 | "HeaderHero",
18 | "settings.json"
19 | );
20 |
21 | static AppSettings Load()
22 | {
23 | try
24 | {
25 | if (File.Exists(SettingsPath))
26 | {
27 | var json = File.ReadAllText(SettingsPath);
28 | using var doc = JsonDocument.Parse(json);
29 | if (doc.RootElement.TryGetProperty("LastProject", out var prop))
30 | {
31 | return new AppSettings {LastProject = prop.GetString() ?? ""};
32 | }
33 | }
34 | }
35 | catch
36 | {
37 | // ignore errors, return default
38 | }
39 | return new AppSettings();
40 | }
41 |
42 | public void Save()
43 | {
44 | try
45 | {
46 | var dir = Path.GetDirectoryName(SettingsPath)!;
47 | if (dir != null && !Directory.Exists(dir))
48 | Directory.CreateDirectory(dir);
49 | using var stream = File.Create(SettingsPath);
50 | using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true });
51 | writer.WriteStartObject();
52 | writer.WriteString("LastProject", LastProject);
53 | writer.WriteEndObject();
54 | }
55 | catch
56 | {
57 | // ignore errors
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/header_hero/Parser/Analytics.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 |
4 | namespace HeaderHero.Parser;
5 |
6 | public class ItemAnalytics
7 | {
8 | public readonly HashSet AllIncludes = [];
9 | public int TotalIncludeLines;
10 | public readonly HashSet AllIncludedBy = [];
11 | public readonly HashSet TranslationUnitsIncludedBy = [];
12 | public bool Analyzed;
13 | }
14 |
15 | public class Analytics
16 | {
17 | public readonly Dictionary Items = new();
18 |
19 | public static Analytics Analyze(Data.Project project)
20 | {
21 | Analytics analytics = new Analytics();
22 | foreach (var kvp in project.Files)
23 | analytics.Analyze(kvp.Key, project);
24 | return analytics;
25 | }
26 |
27 | ItemAnalytics Analyze(string path, Data.Project project)
28 | {
29 | if (!Items.TryGetValue(path, out var a))
30 | {
31 | a = new ItemAnalytics();
32 | Items.Add(path, a);
33 | }
34 | if (a.Analyzed)
35 | return a;
36 | a.Analyzed = true;
37 |
38 | Data.SourceFile sf = project.Files[path];
39 | foreach (string include in sf.AbsoluteIncludes)
40 | {
41 | if (include == path)
42 | continue;
43 |
44 | bool is_tu = Data.SourceFile.IsTranslationUnitPath(path);
45 |
46 | ItemAnalytics ai = Analyze(include, project);
47 | a.AllIncludes.Add(include);
48 | ai.AllIncludedBy.Add(path);
49 | if (is_tu)
50 | ai.TranslationUnitsIncludedBy.Add (path);
51 |
52 |
53 | a.AllIncludes.UnionWith(ai.AllIncludes);
54 | foreach (string inc in ai.AllIncludes) {
55 | Items[inc].AllIncludedBy.Add(path);
56 | if (is_tu)
57 | Items[inc].TranslationUnitsIncludedBy.Add (path);
58 | }
59 | }
60 |
61 | a.TotalIncludeLines = a.AllIncludes.Sum(f => project.Files[f].Lines);
62 | return a;
63 | }
64 | }
--------------------------------------------------------------------------------
/header_hero/MainWindow.axaml:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Header Hero (Fork/Rewrite)
2 |
3 | A tool for analyzing C/C++ codebase header dependencies. Header Hero recursively scans source code directories and maps out
4 | `#include` relationships. It can point out the "worst" headers in terms of total aggregate line count, or "most included" headers
5 | where changing them would cause the most recompiles. Hopefully it can help you reduce the header inclusion issues.
6 |
7 | This is a fork/rewrite of an old tool made by (now defunct) BitSquid game engine back in 2011, see
8 | [Caring by Sharing: Header Hero](https://bitsquid.blogspot.com/2011/10/caring-by-sharing-header-hero.html) blog post
9 | ([webarchive link](https://web.archive.org/web/20250430235425/https://bitsquid.blogspot.com/2011/10/caring-by-sharing-header-hero.html)).
10 | The original Bitbucket repository of that tool is long gone by now.
11 |
12 | Back in 2018 I made some functionality and UX improvements to it, see
13 | [Header Hero Improvements](https://aras-p.info/blog/2018/01/17/Header-Hero-Improvements/) blog post.
14 |
15 | In 2025 I rewrote the tool from C# WinForms to [Avalonia UI](https://avaloniaui.net/) framework, using a more modern C#/.NET
16 | version as well. So this now works on Windows, Mac and Linux. I did some more UI improvements and performance optimizations too;
17 | header scanning on a large codebase is several times faster than before now.
18 |
19 | ## Usage
20 |
21 | Specify where your project is at, as well as where are the source files under (these folders are scanned recursively),
22 | and where are the include folders. Each line there can be an absolute path, or a path relative to project root.
23 |
24 | You can specify a precompiled header (absolute path), if you use one. The tool tries to detect "system" (compiler / platform SDK)
25 | includes automatically and displays them at the bottom.
26 |
27 | 
28 |
29 | Press the "Scan" button! This will show up the main report window. See the above mentioned blog post for details on what it contains.
30 |
31 | | Main | Includes | Errors | Missing |
32 | |---|---|---|---|
33 | |  |  |  |  |
34 |
35 | ## Building
36 |
37 | I have used Jetbrains Rider to build the solution under `header_hero/header_hero.sln`. It is set to use .NET 9, so you might need to install that. Otherwise just build and run;
38 | things Should Work, hopefully. A different C# IDE or some sort of command line build might work too, but I did not try that.
39 |
--------------------------------------------------------------------------------
/header_hero/Parser/Parser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using System.IO;
5 |
6 | namespace HeaderHero.Parser;
7 |
8 | static class Parser
9 | {
10 | public class Result
11 | {
12 | public List SystemIncludes {get;} = [];
13 | public List LocalIncludes {get;} = [];
14 | public int Lines;
15 | }
16 |
17 | static bool WordAt(string s, int index, string word)
18 | {
19 | if (index < 0 || index + word.Length > s.Length)
20 | return false;
21 |
22 | return s.AsSpan(index, word.Length).SequenceEqual(word);
23 | }
24 |
25 | static bool SkipSpace(string s, ref int i)
26 | {
27 | while (i < s.Length && char.IsWhiteSpace(s[i]))
28 | ++i;
29 | if (i >= s.Length)
30 | return false;
31 | return true;
32 | }
33 |
34 | static bool ParseLine(string line, Result result)
35 | {
36 | int i = 0;
37 | if (!SkipSpace(line, ref i))
38 | return true;
39 |
40 | // is a preprocessor macro?
41 | if (line[i] != '#')
42 | return true;
43 | ++i;
44 |
45 | if (!SkipSpace(line, ref i))
46 | return true;
47 |
48 | // has "include"?
49 | string keyword = "include";
50 | if (!WordAt(line, i, keyword))
51 | return true;
52 | i += keyword.Length;
53 | if (i >= line.Length || (!char.IsWhiteSpace(line[i]) && line[i] != '<' && line[i] != '"'))
54 | return false; // malformed include
55 | if (!SkipSpace(line, ref i))
56 | return false; // malformed include
57 |
58 | // angled or quoted?
59 | bool angled;
60 | if (line[i] == '<')
61 | angled = true;
62 | else if (line[i] == '"')
63 | angled = false;
64 | else
65 | return false; // malformed include or include w/ a define
66 | ++i;
67 |
68 | // scan the name
69 | int nameStart = i;
70 | while (i < line.Length)
71 | {
72 | if (angled && line[i] == '>')
73 | {
74 | int nameEnd = i - 1;
75 | if (nameEnd == nameStart)
76 | return false; // empty include
77 | result.SystemIncludes.Add(line.Substring(nameStart, nameEnd-nameStart+1));
78 | return true;
79 | }
80 | if (!angled && line[i] == '"')
81 | {
82 | int nameEnd = i - 1;
83 | if (nameEnd == nameStart)
84 | return false; // empty include
85 | result.LocalIncludes.Add(line.Substring(nameStart, nameEnd-nameStart+1));
86 | return true;
87 | }
88 | ++i;
89 | }
90 |
91 | return false; // non-terminated include name
92 | }
93 |
94 | public static Result ParseFile(string fullPath, List errors)
95 | {
96 | Result res = new Result();
97 | string[] lines = File.ReadAllLines(fullPath, Encoding.UTF8);
98 | res.Lines = lines.Length;
99 | foreach (string line in lines)
100 | {
101 | if (!ParseLine(line, res))
102 | errors.Add(new ScanError($"Could not parse: {line}", fullPath));
103 | }
104 | return res;
105 | }
106 | }
--------------------------------------------------------------------------------
/header_hero/Data/Project.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Text.Json;
7 |
8 | namespace HeaderHero.Data;
9 |
10 | public class Project
11 | {
12 | public string ProjectRoot { get; set; } = string.Empty;
13 | public List ScanDirectories { get; set; } = [];
14 | public List IncludeDirectories { get; set; } = [];
15 | public string PrecompiledHeader { get; set; } = string.Empty;
16 | public Dictionary Files { get; } = new();
17 | public TimeSpan ScanTime { get; set; }
18 |
19 | public IEnumerable ScanDirectoriesRooted()
20 | {
21 | return ScanDirectories.Select(s => Path.IsPathFullyQualified(s) ? s : Path.Combine(ProjectRoot, s));
22 | }
23 | public IEnumerable IncludeDirectoriesRooted()
24 | {
25 | return IncludeDirectories.Select(s => Path.IsPathFullyQualified(s) ? s : Path.Combine(ProjectRoot, s));
26 | }
27 |
28 | public string ToJson(List systemIncludesToExclude)
29 | {
30 | using var stream = new MemoryStream();
31 | using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions {Indented = true} );
32 | writer.WriteStartObject();
33 | writer.WriteString("ProjectRoot", ProjectRoot);
34 | writer.WriteStartArray("ScanDirectories");
35 | foreach (var s in ScanDirectories)
36 | writer.WriteStringValue(s);
37 | writer.WriteEndArray();
38 | writer.WriteStartArray("IncludeDirectories");
39 | foreach (var s in IncludeDirectories)
40 | {
41 | if (!systemIncludesToExclude.Contains(s))
42 | writer.WriteStringValue(s);
43 | }
44 | writer.WriteEndArray();
45 | writer.WriteString("PrecompiledHeader", PrecompiledHeader);
46 | writer.WriteEndObject();
47 | writer.Flush();
48 | return Encoding.UTF8.GetString(stream.ToArray());
49 | }
50 |
51 | public void FromJsonFile(string filePath)
52 | {
53 | ProjectRoot = string.Empty;
54 | ScanDirectories = [];
55 | IncludeDirectories = [];
56 | PrecompiledHeader = string.Empty;
57 | Files.Clear();
58 |
59 | try
60 | {
61 | var json = File.ReadAllText(filePath);
62 | using var doc = JsonDocument.Parse(json);
63 | ProjectRoot = GetString("ProjectRoot", doc.RootElement) ?? string.Empty;
64 | ScanDirectories = GetStringList("ScanDirectories", doc.RootElement);
65 | IncludeDirectories = GetStringList("IncludeDirectories", doc.RootElement);
66 | PrecompiledHeader = GetString("PrecompiledHeader", doc.RootElement) ?? string.Empty;
67 | }
68 | catch (Exception ex)
69 | {
70 | Console.WriteLine($"Failed to load JSON from {filePath}: {ex.Message}");
71 | }
72 | }
73 |
74 | static string GetString(string key, JsonElement element)
75 | {
76 | if (element.TryGetProperty(key, out var prop) && prop.ValueKind == JsonValueKind.String)
77 | {
78 | return prop.GetString();
79 | }
80 | return null;
81 | }
82 |
83 | static List GetStringList(string key, JsonElement element)
84 | {
85 | List res = [];
86 | if (element.TryGetProperty(key, out var prop) && prop.ValueKind == JsonValueKind.Array)
87 | {
88 | foreach (var o in prop.EnumerateArray())
89 | {
90 | if (o.ValueKind == JsonValueKind.String)
91 | res.Add(o.GetString());
92 | }
93 | }
94 | return res;
95 | }
96 | }
--------------------------------------------------------------------------------
/header_hero/Parser/Report.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Globalization;
3 | using System.Linq;
4 | using System.Text;
5 | using System.IO;
6 |
7 | namespace HeaderHero.Parser;
8 |
9 | public class ReportFile(int count, string path)
10 | {
11 | public int count { get; set; } = count;
12 | public string path { get; } = path;
13 | public string name { get; set; } = Path.GetFileName(path);
14 | }
15 |
16 | class Report
17 | {
18 | readonly Data.Project _project;
19 | readonly Analytics _analytics;
20 |
21 | public readonly string summary;
22 | public readonly List largest;
23 | public readonly List hubs;
24 | public readonly List precompiled;
25 |
26 | public Report(Data.Project project, Analytics analytics)
27 | {
28 | _project = project;
29 | _analytics = analytics;
30 | summary = GenerateSummary();
31 | largest = GenerateLargestContributors();
32 | hubs = GenerateHeaderHubs();
33 | precompiled = GeneratePrecompiled();
34 | }
35 |
36 | List AppendFileList(IEnumerable> count)
37 | {
38 | return count.Select(kvp => new ReportFile(kvp.Value, kvp.Key)).ToList();
39 | }
40 |
41 | string GenerateSummary()
42 | {
43 | int pch_lines = _project.Files.Where(kvp => kvp.Value.Precompiled).Sum(kvp => kvp.Value.Lines);
44 | int total_lines = _project.Files.Sum(kvp => kvp.Value.Lines) - pch_lines;
45 | int total_parsed = _analytics.Items
46 | .Where (kvp => Data.SourceFile.IsTranslationUnitPath(kvp.Key) && !_project.Files[kvp.Key].Precompiled)
47 | .Sum(kvp => kvp.Value.TotalIncludeLines + _project.Files[kvp.Key].Lines);
48 | float factor = total_lines == 0 ? 0.0f : total_parsed / (float)total_lines;
49 |
50 | const int valueWidth = 13;
51 | var nfi = (NumberFormatInfo)CultureInfo.InvariantCulture.NumberFormat.Clone();
52 | nfi.NumberGroupSeparator = " ";
53 |
54 | StringBuilder sb = new StringBuilder();
55 | sb.AppendLine($"Files {_project.Files.Count.ToString("N0",nfi),valueWidth} (scanned in {_project.ScanTime.TotalSeconds:0.0} sec)");
56 | sb.AppendLine($"Total Lines {total_lines.ToString("N0",nfi),valueWidth}");
57 | sb.AppendLine($"Total Precompiled {pch_lines.ToString("N0",nfi),valueWidth}");
58 | sb.AppendLine($"Total Parsed {total_parsed.ToString("N0",nfi),valueWidth}");
59 | sb.AppendLine($"Blowup Factor {factor.ToString("0.00",nfi),valueWidth}");
60 | return sb.ToString();
61 | }
62 |
63 | List GenerateLargestContributors()
64 | {
65 | var most = _analytics.Items
66 | .ToDictionary(kvp => kvp.Key, kvp => _project.Files[kvp.Key].Lines *
67 | kvp.Value.TranslationUnitsIncludedBy.Count)
68 | .Where(kvp => !_project.Files[kvp.Key].Precompiled)
69 | .Where(kvp => kvp.Value > 0)
70 | .OrderByDescending(kvp => kvp.Value);
71 | return AppendFileList(most);
72 | }
73 |
74 | List GenerateHeaderHubs()
75 | {
76 | var hhubs = _analytics.Items
77 | .ToDictionary(kvp => kvp.Key, kvp => kvp.Value.AllIncludes.Count * kvp.Value.TranslationUnitsIncludedBy.Count)
78 | .Where(kvp => kvp.Value > 0)
79 | .OrderByDescending(kvp => kvp.Value);
80 | return AppendFileList(hhubs);
81 | }
82 |
83 | List GeneratePrecompiled()
84 | {
85 | var pch = _project.Files
86 | .Where(kvp => kvp.Value.Precompiled)
87 | .ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Lines)
88 | .OrderByDescending(kvp => kvp.Value);
89 | return AppendFileList(pch);
90 | }
91 | }
--------------------------------------------------------------------------------
/header_hero/ReportWindow.axaml.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.IO;
3 | using System.Linq;
4 | using Avalonia.Controls;
5 | using Avalonia.Input;
6 | using HeaderHero.Parser;
7 |
8 | namespace HeaderHero;
9 |
10 | public record IncludeRow(string Name, string FullPath, int Count, int Lines);
11 | public record MissingFilesRow(string Name, string Origin);
12 |
13 | public partial class ReportWindow : Window
14 | {
15 | Data.Project _project;
16 | Analytics _analytics;
17 | Scanner _scanner;
18 | readonly LinkedList _history = [];
19 |
20 | public ReportWindow(Data.Project project, Scanner scanner)
21 | {
22 | _history.Clear();
23 | _project = project;
24 | _scanner = scanner;
25 | InitializeComponent();
26 |
27 | GetTopLevel(this)!.Cursor = new Cursor(StandardCursorType.Wait);
28 | Setup(project, scanner);
29 | GetTopLevel(this)!.Cursor = new Cursor(StandardCursorType.Arrow);
30 | }
31 |
32 | void ReportFileList_OnDoubleTapped(object sender, TappedEventArgs e)
33 | {
34 | if (sender is not ListBox lb) return;
35 | if (lb.SelectedItem is not ReportFile rf) return;
36 | Inspect(rf.path);
37 | Tabs.SelectedIndex = 1;
38 | }
39 |
40 | void IncludesList_OnDoubleTapped(object sender, TappedEventArgs e)
41 | {
42 | if (sender is not DataGrid dg) return;
43 | if (dg.SelectedItem is not IncludeRow row) return;
44 | Inspect(row.FullPath);
45 | }
46 |
47 | void BackButton_OnClick(object sender, Avalonia.Interactivity.RoutedEventArgs e)
48 | {
49 | if (_history.Count > 0)
50 | _history.RemoveLast();
51 | if (_history.Count > 0)
52 | Inspect(_history.Last!.Value);
53 | }
54 |
55 | void Inspect(string file)
56 | {
57 | if (_history.Count == 0 || _history.Last() != file)
58 | {
59 | _history.AddLast(file);
60 | if (_history.Count > 10)
61 | _history.RemoveFirst();
62 | }
63 |
64 | // center text
65 | {
66 | var projectFile = _project.Files[file];
67 | var analyticsFile = _analytics.Items[file];
68 | var fileLines = projectFile.Lines;
69 | var directLines = projectFile.AbsoluteIncludes.Sum(f => _project.Files[f].Lines);
70 | var directCount = projectFile.AbsoluteIncludes.Count;
71 | var totalLines = analyticsFile.TotalIncludeLines;
72 | var totalCount = analyticsFile.AllIncludes.Count;
73 | string text = $"{Path.GetFileName(file)}\r\n\r\nLines: {fileLines}\r\nDirect Includes: {directLines} lines, {directCount} files\r\nTotal Includes: {totalLines} lines, {totalCount} files";
74 | CurrentFileLabel.Text = Path.GetFileName(file);
75 | FileDetailsText.Text = text;
76 | }
77 |
78 | // left panel
79 | {
80 | List list = _project.Files[file].AbsoluteIncludes
81 | .OrderByDescending(f => _analytics.Items[f].AllIncludes.Count)
82 | .Select(s => new IncludeRow(Path.GetFileName(s), s, _analytics.Items[s].AllIncludes.Count, _analytics.Items[s].TotalIncludeLines)).ToList();
83 | IncludesList.ItemsSource = list;
84 | }
85 |
86 | // right panel
87 | {
88 | IEnumerable included = _project.Files.Where(kvp => kvp.Value.AbsoluteIncludes.Contains(file)).Select(kvp => kvp.Key);
89 | List list = included.OrderByDescending(s => _analytics.Items[s].AllIncludedBy.Count)
90 | .Select(s => new IncludeRow(Path.GetFileName(s), s, _analytics.Items[s].AllIncludedBy.Count, _analytics.Items[s].TotalIncludeLines)).ToList();
91 | IncludedByList.ItemsSource = list;
92 | }
93 | }
94 |
95 | void Setup(Data.Project project, Scanner scanner)
96 | {
97 | _history.Clear();
98 | _project = project;
99 | _scanner = scanner;
100 | _analytics = Analytics.Analyze(_project);
101 |
102 | ErrorsList.ItemsSource = scanner.Errors;
103 | if (scanner.Errors.Count > 0)
104 | ErrorsTab.Header += $" ({scanner.Errors.Count})";
105 |
106 | var notFoundItems = scanner.NotFound
107 | .OrderBy(s => s)
108 | .Select(s => new MissingFilesRow(s, _scanner.NotFoundOrigins[s]))
109 | .ToList();
110 | MissingFilesList.ItemsSource = notFoundItems;
111 | if (notFoundItems.Count > 0)
112 | MissingTab.Header += $" ({notFoundItems.Count})";
113 |
114 | Report rpt = new Report(_project, _analytics);
115 | SummaryText.Text = rpt.summary;
116 |
117 | BiggestList.ItemsSource = rpt.largest;
118 | HeaderHubsList.ItemsSource = rpt.hubs;
119 | PrecompiledHeadersList.ItemsSource = rpt.precompiled;
120 | }
121 | }
--------------------------------------------------------------------------------
/header_hero/Utils/SystemIncludesLocator.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Runtime.InteropServices;
7 | using System.Runtime.Versioning;
8 | using System.Text.Json;
9 |
10 | namespace HeaderHero.Utils;
11 |
12 | public static class SystemIncludesLocator
13 | {
14 | public static List FindSystemIncludes()
15 | {
16 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
17 | return FindLinuxIncludes();
18 | if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
19 | return FindMacIncludes();
20 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
21 | return FindWindowsIncludes();
22 | return [];
23 | }
24 |
25 | static List FindLinuxIncludes()
26 | {
27 | List res = [];
28 | string[] common =
29 | [
30 | "/usr/include",
31 | "/usr/local/include",
32 | "/usr/include/x86_64-linux-gnu",
33 | "/usr/lib/gcc"
34 | ];
35 | res.AddRange(common.Where(Directory.Exists));
36 |
37 | string cpp = "/usr/include/c++";
38 | if (Directory.Exists(cpp))
39 | {
40 | var latest = Directory.GetDirectories(cpp).OrderByDescending(Path.GetFileName).FirstOrDefault();
41 | if (latest != null)
42 | res.Add(latest);
43 | }
44 | return res;
45 | }
46 |
47 | static List FindMacIncludes()
48 | {
49 | string[] common =
50 | [
51 | "/usr/include",
52 | "/Library/Developer/CommandLineTools/usr/include",
53 | "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include",
54 | "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include",
55 | "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1",
56 | ];
57 | return common.Where(Directory.Exists).ToList();
58 | }
59 |
60 | [SupportedOSPlatform("windows")]
61 | static List FindWindowsIncludes()
62 | {
63 | var paths = new List();
64 | var msvcInclude = GetMsvcIncludePath();
65 | if (msvcInclude != null)
66 | paths.Add(msvcInclude);
67 | var sdkIncludes = GetWindowsSdkIncludePaths();
68 | paths.AddRange(sdkIncludes);
69 | return paths;
70 | }
71 |
72 | // Starting with Visual Studio 2017, "vswhere" utility
73 | // is used for location detections, see
74 | // https://devblogs.microsoft.com/cppblog/finding-the-visual-c-compiler-tools-in-visual-studio-2017/
75 | [SupportedOSPlatform("windows")]
76 | static string FindVsWhere()
77 | {
78 | var progFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
79 | if (string.IsNullOrEmpty(progFiles))
80 | return null;
81 | var path = Path.Combine(progFiles, @"Microsoft Visual Studio\Installer\vswhere.exe");
82 | return File.Exists(path) ? path : null;
83 | }
84 |
85 | [SupportedOSPlatform("windows")]
86 | static string GetLatestVsInstallPath()
87 | {
88 | var whereTool = FindVsWhere();
89 | if (whereTool == null)
90 | return null;
91 |
92 | var psi = new ProcessStartInfo
93 | {
94 | FileName = whereTool,
95 | Arguments = "-latest -format json -prerelease -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
96 | RedirectStandardOutput = true,
97 | UseShellExecute = false
98 | };
99 | using var proc = Process.Start(psi);
100 | if (proc == null)
101 | return null;
102 | var json = proc.StandardOutput.ReadToEnd();
103 | proc.WaitForExit();
104 | if (string.IsNullOrWhiteSpace(json))
105 | return null;
106 |
107 | using var doc = JsonDocument.Parse(json);
108 | var root = doc.RootElement;
109 | if (root.ValueKind != JsonValueKind.Array || root.GetArrayLength() == 0)
110 | return null;
111 | var elem = root[0];
112 | if (elem.TryGetProperty("installationPath", out var pathProp))
113 | return pathProp.GetString();
114 | return null;
115 | }
116 |
117 | [SupportedOSPlatform("windows")]
118 | static string GetMsvcIncludePath()
119 | {
120 | var vs = GetLatestVsInstallPath();
121 | if (vs == null)
122 | return null;
123 | var tools = Path.Combine(vs, @"VC\Tools\MSVC");
124 | if (!Directory.Exists(tools))
125 | return null;
126 | var latest = Directory.GetDirectories(tools).OrderByDescending(Path.GetFileName).FirstOrDefault();
127 | if (latest == null)
128 | return null;
129 | return Path.Combine(latest, "include");
130 | }
131 |
132 | [SupportedOSPlatform("windows")]
133 | static List GetWindowsSdkIncludePaths()
134 | {
135 | using var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(
136 | @"SOFTWARE\Microsoft\Windows Kits\Installed Roots");
137 |
138 | if (key?.GetValue("KitsRoot10") is not string root)
139 | return [];
140 |
141 | var incRoot = Path.Combine(root, "Include");
142 | if (!Directory.Exists(incRoot))
143 | return [];
144 |
145 | var latest = Directory.GetDirectories(incRoot).OrderByDescending(Path.GetFileName).FirstOrDefault();
146 | if (latest == null)
147 | return [];
148 |
149 | return
150 | [
151 | Path.Combine(latest, "ucrt"),
152 | Path.Combine(latest, "um"),
153 | Path.Combine(latest, "shared"),
154 | Path.Combine(latest, "winrt")
155 | ];
156 | }
157 | }
--------------------------------------------------------------------------------
/img/hh_blender_win.json:
--------------------------------------------------------------------------------
1 | {
2 | "ProjectRoot": "C:\\code\\projects\\other\\blender\\blender",
3 | "ScanDirectories": [
4 | "source",
5 | "extern",
6 | "intern"
7 | ],
8 | "IncludeDirectories": [
9 | "extern\\audaspace\\bindings",
10 | "extern\\audaspace\\bindings\\C",
11 | "extern\\audaspace\\include",
12 | "extern\\bullet2\\src",
13 | "extern\\ceres\\include",
14 | "extern\\ceres\\internal",
15 | "extern\\curve_fit_nd",
16 | "extern\\draco\\draco\\src",
17 | "extern\\Eigen3",
18 | "extern\\fast_float",
19 | "extern\\fmtlib\\include",
20 | "extern\\glog\\include",
21 | "extern\\gmock",
22 | "extern\\gmock\\include",
23 | "extern\\gtest",
24 | "extern\\gtest\\include",
25 | "extern\\mantaflow",
26 | "extern\\mantaflow\\helper\\util",
27 | "extern\\mantaflow\\preprocessed",
28 | "extern\\quadriflow\\3rd\\lemon-1.3.1",
29 | "extern\\quadriflow\\src",
30 | "extern\\ufbx",
31 | "extern\\vulkan_memory_allocator",
32 | "extern\\xxhash",
33 | "intern",
34 | "intern\\atomic",
35 | "intern\\clog",
36 | "intern\\cycles",
37 | "intern\\eigen",
38 | "intern\\ghost",
39 | "intern\\guardedalloc",
40 | "intern\\itasc",
41 | "intern\\libmv",
42 | "intern\\mantaflow\\extern",
43 | "intern\\memutil",
44 | "intern\\mikktspace",
45 | "intern\\opensubdiv",
46 | "intern\\openvdb",
47 | "intern\\rigidbody",
48 | "intern\\utfconv",
49 | "lib\\windows_x64\\alembic\\include",
50 | "lib\\windows_x64\\embree\\include",
51 | "lib\\windows_x64\\epoxy\\include",
52 | "lib\\windows_x64\\ffmpeg\\include",
53 | "lib\\windows_x64\\fftw3\\include",
54 | "lib\\windows_x64\\freetype\\include",
55 | "lib\\windows_x64\\Fribidi\\include",
56 | "lib\\windows_x64\\gmp\\include",
57 | "lib\\windows_x64\\harfbuzz\\include",
58 | "lib\\windows_x64\\haru\\include",
59 | "lib\\windows_x64\\imath\\include",
60 | "lib\\windows_x64\\imath\\include\\Imath",
61 | "lib\\windows_x64\\jpeg\\include",
62 | "lib\\windows_x64\\manifold\\include",
63 | "lib\\windows_x64\\MaterialX\\include",
64 | "lib\\windows_x64\\openal\\include\\AL",
65 | "lib\\windows_x64\\opencolorio\\include",
66 | "lib\\windows_x64\\openexr\\include",
67 | "lib\\windows_x64\\openimagedenoise\\include",
68 | "lib\\windows_x64\\OpenImageIO\\include",
69 | "lib\\windows_x64\\opensubdiv\\include",
70 | "lib\\windows_x64\\openvdb\\include",
71 | "lib\\windows_x64\\osl\\include",
72 | "lib\\windows_x64\\python\\311\\include",
73 | "lib\\windows_x64\\tbb\\include",
74 | "lib\\windows_x64\\usd\\include",
75 | "lib\\windows_x64\\vulkan\\include",
76 | "lib\\windows_x64\\xml2\\include",
77 | "lib\\windows_x64\\zlib\\include",
78 | "lib\\windows_x64\\zstd\\include",
79 | "source\\blender",
80 | "source\\blender\\animrig",
81 | "source\\blender\\asset_system",
82 | "source\\blender\\asset_system\\intern",
83 | "source\\blender\\asset_system\\intern\\library_types",
84 | "source\\blender\\blenfont",
85 | "source\\blender\\blenkernel",
86 | "source\\blender\\blenlib",
87 | "source\\blender\\blenloader",
88 | "source\\blender\\blenloader_core",
89 | "source\\blender\\blentranslation",
90 | "source\\blender\\bmesh",
91 | "source\\blender\\compositor",
92 | "source\\blender\\compositor\\algorithms",
93 | "source\\blender\\compositor\\cached_resources",
94 | "source\\blender\\compositor\\derived_resources",
95 | "source\\blender\\compositor\\utilities",
96 | "source\\blender\\depsgraph",
97 | "source\\blender\\draw",
98 | "source\\blender\\draw\\intern",
99 | "source\\blender\\editors\\asset",
100 | "source\\blender\\editors\\include",
101 | "source\\blender\\editors\\sculpt_paint",
102 | "source\\blender\\editors\\space_graph",
103 | "source\\blender\\editors\\space_sequencer",
104 | "source\\blender\\freestyle",
105 | "source\\blender\\functions",
106 | "source\\blender\\geometry",
107 | "source\\blender\\gpu",
108 | "source\\blender\\gpu\\intern",
109 | "source\\blender\\gpu\\metal",
110 | "source\\blender\\gpu\\opengl",
111 | "source\\blender\\gpu\\shaders\\infos",
112 | "source\\blender\\gpu\\vulkan",
113 | "source\\blender\\ikplugin",
114 | "source\\blender\\imbuf",
115 | "source\\blender\\imbuf\\intern",
116 | "source\\blender\\imbuf\\intern\\openexr",
117 | "source\\blender\\imbuf\\intern\\oiio",
118 | "source\\blender\\imbuf\\movie",
119 | "source\\blender\\imbuf\\movie\\intern",
120 | "source\\blender\\imbuf\\opencolorio",
121 | "source\\blender\\io\\alembic",
122 | "source\\blender\\io\\common",
123 | "source\\blender\\io\\csv",
124 | "source\\blender\\io\\fbx",
125 | "source\\blender\\io\\fbx\\importer",
126 | "source\\blender\\io\\ply",
127 | "source\\blender\\io\\ply\\exporter",
128 | "source\\blender\\io\\ply\\importer",
129 | "source\\blender\\io\\ply\\intern",
130 | "source\\blender\\io\\stl",
131 | "source\\blender\\io\\usd",
132 | "source\\blender\\io\\usd\\intern",
133 | "source\\blender\\io\\wavefront_obj",
134 | "source\\blender\\io\\wavefront_obj\\exporter",
135 | "source\\blender\\io\\wavefront_obj\\importer",
136 | "source\\blender\\makesdna",
137 | "source\\blender\\makesrna",
138 | "source\\blender\\modifiers",
139 | "source\\blender\\nodes",
140 | "source\\blender\\nodes\\composite",
141 | "source\\blender\\nodes\\function\\include",
142 | "source\\blender\\nodes\\geometry",
143 | "source\\blender\\nodes\\geometry\\include",
144 | "source\\blender\\nodes\\intern",
145 | "source\\blender\\nodes\\shader",
146 | "source\\blender\\nodes\\texture",
147 | "source\\blender\\python",
148 | "source\\blender\\python\\intern",
149 | "source\\blender\\render",
150 | "source\\blender\\render\\intern",
151 | "source\\blender\\sequencer",
152 | "source\\blender\\shader_fx",
153 | "source\\blender\\simulation",
154 | "source\\blender\\windowmanager",
155 | "source\\blender\\windowmanager\\gizmo",
156 | "source\\blender\\windowmanager\\xr"
157 | ],
158 | "PrecompiledHeader": "C:\\code\\projects\\other\\blender\\blender\\pchtest.h"
159 | }
--------------------------------------------------------------------------------
/header_hero/MainWindow.axaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 | using Avalonia.Controls;
7 | using Avalonia.Interactivity;
8 | using Avalonia.Platform.Storage;
9 | using Avalonia.Threading;
10 | using HeaderHero.Utils;
11 |
12 | namespace HeaderHero;
13 |
14 | public partial class MainWindow : Window
15 | {
16 | static readonly FilePickerFileType[] PickerFileTypes =
17 | [
18 | new("JSON") { Patterns = ["*.json"] }
19 | ];
20 |
21 | string _curProjectPath;
22 | string _lastSaveProjectState;
23 | Data.Project _project = new();
24 | readonly List _systemIncludes;
25 |
26 | public MainWindow()
27 | {
28 | InitializeComponent();
29 | if (OperatingSystem.IsMacOS())
30 | MenuBar.IsVisible = false;
31 |
32 | Closing += OnWindowClosing;
33 |
34 | _systemIncludes = SystemIncludesLocator.FindSystemIncludes();
35 | SystemIncludeDirsTextBox.Text = string.Join("\n", _systemIncludes);
36 |
37 | //@TODO
38 | //projectDirsTextBox.MouseDoubleClick += (_1, _2) => scan_AddDirectory_Click(_1, null);
39 | //includeDirsTextBox.MouseDoubleClick += (_1, _2) => include_AddDirectory_Click(_1, null);
40 |
41 | _lastSaveProjectState = _project.ToJson(_systemIncludes);
42 |
43 | var settings = AppSettings.Instance;
44 | var lastProject = settings.LastProject;
45 | if (!string.IsNullOrEmpty(lastProject) && File.Exists(lastProject))
46 | {
47 | Open(lastProject);
48 | }
49 | }
50 |
51 | void ProjectFieldsToUI()
52 | {
53 | RootTextBox.Text = _project.ProjectRoot ?? string.Empty;
54 | ProjectDirsTextBox.Text = string.Join("\r\n", _project.ScanDirectories.ToArray());
55 | IncludeDirsTextBox.Text = string.Join("\r\n", _project.IncludeDirectories.Except(_systemIncludes).ToArray());
56 | PchTextBox.Text = _project.PrecompiledHeader ?? string.Empty;
57 | }
58 |
59 | void ParseProjectFieldsFromUI()
60 | {
61 | _project.ProjectRoot = RootTextBox.Text?.Trim() ?? "";
62 | _project.ScanDirectories = ProjectDirsTextBox.Text?.Split('\n', '\r').Where(s => !string.IsNullOrWhiteSpace(s)).ToList() ?? [];
63 | _project.IncludeDirectories = IncludeDirsTextBox.Text?.Split('\n', '\r').Where(s => !string.IsNullOrWhiteSpace(s)).ToList() ?? [];
64 | _project.IncludeDirectories.AddRange(_systemIncludes);
65 | _project.PrecompiledHeader = PchTextBox.Text?.Trim() ?? "";
66 | }
67 |
68 | void MarkSave()
69 | {
70 | if (_curProjectPath != null)
71 | Title = "Header Hero - " + _curProjectPath;
72 | else
73 | Title = "Header Hero";
74 | _lastSaveProjectState = _project.ToJson(_systemIncludes);
75 | }
76 |
77 | async Task AskSaveProject()
78 | {
79 | ParseProjectFieldsFromUI();
80 | if (_lastSaveProjectState == _project.ToJson(_systemIncludes))
81 | return true;
82 |
83 | int choice = await new MessageBox3("Save Project?", "Project was modified, save changes?", "Save", "Do not save", "Cancel").ShowDialog(this);
84 | switch (choice)
85 | {
86 | case 0: return await SaveProject(false);
87 | case 1: break;
88 | case 2: return false;
89 | }
90 | return true;
91 | }
92 |
93 | async void NewProject()
94 | {
95 | if (!await AskSaveProject())
96 | return;
97 |
98 | AppSettings.Instance.LastProject = string.Empty;
99 | AppSettings.Instance.Save();
100 | _curProjectPath = null;
101 | _project = new Data.Project();
102 | ProjectFieldsToUI();
103 | MarkSave();
104 | }
105 |
106 | async void OpenProject()
107 | {
108 | if (!await AskSaveProject())
109 | return;
110 |
111 | var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions {AllowMultiple = false, FileTypeFilter = PickerFileTypes});
112 | if (files is not {Count: > 0})
113 | return;
114 |
115 | var path = files[0].TryGetLocalPath();
116 | AppSettings.Instance.LastProject = path;
117 | AppSettings.Instance.Save();
118 | Open(path);
119 | }
120 |
121 | void Open(string path)
122 | {
123 | _curProjectPath = path;
124 | _project = new Data.Project();
125 | _project.FromJsonFile(path);
126 | MarkSave();
127 | ProjectFieldsToUI();
128 | }
129 |
130 | async Task SaveProject(bool force_save_as)
131 | {
132 | if (_curProjectPath == null || force_save_as)
133 | {
134 | var result = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions {SuggestedFileName = _curProjectPath != null ? Path.GetFileName(_curProjectPath) : "header_hero_project.json", FileTypeChoices = PickerFileTypes});
135 | if (result == null)
136 | return false;
137 | _curProjectPath = result.TryGetLocalPath();
138 | }
139 |
140 | if (_curProjectPath == null)
141 | return false;
142 |
143 | AppSettings.Instance.LastProject = _curProjectPath;
144 | AppSettings.Instance.Save();
145 | ParseProjectFieldsFromUI();
146 | File.WriteAllText(_curProjectPath, _project.ToJson(_systemIncludes));
147 | MarkSave();
148 | return true;
149 | }
150 |
151 | bool _isCloseRequested;
152 |
153 | void OnWindowClosing(object sender, WindowClosingEventArgs e)
154 | {
155 | if (_isCloseRequested)
156 | return;
157 | e.Cancel = true;
158 |
159 | // Since project save ask will show async dialogs, we have to do that in regular
160 | // UI loop.
161 | Dispatcher.UIThread.Post(async () =>
162 | {
163 | bool allow = await AskSaveProject();
164 | if (allow)
165 | {
166 | _isCloseRequested = true;
167 | Close();
168 | }
169 | });
170 | }
171 |
172 | async void ScanProject()
173 | {
174 | ParseProjectFieldsFromUI();
175 | var scanner = new Parser.Scanner(_project);
176 |
177 | ProgressDialog dlg = new ProgressDialog();
178 | dlg.Start(async (fb) =>
179 | {
180 | scanner.Rescan(fb);
181 | });
182 |
183 | // Poll every 100ms until the dialog closes
184 | var timer = new System.Timers.Timer(100);
185 | timer.Elapsed += (_, _) =>
186 | {
187 | Dispatcher.UIThread.Post(() =>
188 | {
189 | if (!dlg.IsVisible)
190 | timer.Stop();
191 | else
192 | dlg.Poll();
193 | });
194 | };
195 | timer.Start();
196 |
197 | await dlg.ShowDialog(this);
198 |
199 | ProjectFieldsToUI();
200 |
201 | ReportWindow report = new ReportWindow(_project, scanner);
202 | report.Show();
203 | }
204 |
205 | void Menu_NewProject(object sender, EventArgs e)
206 | {
207 | NewProject();
208 | }
209 |
210 | void Menu_OpenProject(object sender, EventArgs e)
211 | {
212 | OpenProject();
213 | }
214 |
215 | async void Menu_SaveProject(object sender, EventArgs e)
216 | {
217 | await SaveProject(false);
218 | }
219 |
220 | async void Menu_SaveProjectAs(object sender, EventArgs e)
221 | {
222 | await SaveProject(true);
223 | }
224 |
225 | void Menu_Quit(object sender, EventArgs e)
226 | {
227 | Close();
228 | }
229 |
230 | void Button_Scan(object sender, RoutedEventArgs e)
231 | {
232 | ScanProject();
233 | }
234 | }
235 |
--------------------------------------------------------------------------------
/header_hero/ReportWindow.axaml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
21 |
26 |
29 |
30 |
31 |
32 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
67 |
68 |
78 |
79 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
99 |
100 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
121 |
122 |
123 |
124 |
131 |
132 |
133 |
134 |
--------------------------------------------------------------------------------
/header_hero/Parser/Scanner.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Linq;
5 | using System.IO;
6 | using HeaderHero.Data;
7 |
8 | namespace HeaderHero.Parser;
9 |
10 | public class ProgressFeedback
11 | {
12 | public string Title = "";
13 | public int Item;
14 | public int Count;
15 | public string Message = "";
16 | }
17 |
18 | public record ScanError(string Message, string Path);
19 |
20 | public class Scanner
21 | {
22 | readonly Project _project;
23 | HashSet _queued;
24 |
25 | HashSet _nextPass;
26 |
27 | Dictionary _system_includes;
28 | Dictionary _file_existence;
29 | bool _scanning_pch;
30 | bool CaseSensitive { get; }
31 |
32 | public List Errors;
33 | public HashSet NotFound;
34 | public Dictionary NotFoundOrigins;
35 |
36 | public Scanner(Project p)
37 | {
38 | _project = p;
39 | CaseSensitive = IsCaseSensitive();
40 | Clear();
41 | }
42 |
43 | void Clear()
44 | {
45 | _project.Files.Clear();
46 |
47 | _queued = [];
48 | _nextPass = [];
49 | _system_includes = [];
50 | _file_existence = [];
51 |
52 | Errors = [];
53 | NotFound = [];
54 | NotFoundOrigins = [];
55 | }
56 |
57 | bool IsCaseSensitive()
58 | {
59 | foreach (string dir in _project.ScanDirectoriesRooted())
60 | {
61 | DirectoryInfo dl = new DirectoryInfo(dir.ToLowerInvariant());
62 | DirectoryInfo du = new DirectoryInfo(dir.ToUpperInvariant());
63 | if (dl.Exists != du.Exists || dl.CreationTime != du.CreationTime || dl.LastAccessTime != du.LastAccessTime)
64 | return true;
65 | }
66 | return false;
67 | }
68 |
69 | public void Rescan(ProgressFeedback feedback)
70 | {
71 | Stopwatch sw = Stopwatch.StartNew();
72 |
73 | Clear();
74 | string[] currentPass;
75 |
76 | feedback.Title = "Scanning precompiled header...";
77 |
78 | // scan everything that goes into precompiled header
79 | _scanning_pch = true;
80 | if (!string.IsNullOrEmpty(_project.PrecompiledHeader) && FileExists(_project.PrecompiledHeader))
81 | {
82 | var inc = Path.GetFullPath(_project.PrecompiledHeader);
83 | ScanFile(inc);
84 |
85 | currentPass = _nextPass.ToArray();
86 | _nextPass = [];
87 | while (currentPass.Length > 0)
88 | {
89 | foreach (string path in currentPass)
90 | ScanFile(path);
91 | currentPass = _nextPass.ToArray();
92 | _nextPass = [];
93 | }
94 | _queued.Clear();
95 | }
96 | _scanning_pch = false;
97 |
98 | feedback.Title = "Scanning directories...";
99 | foreach (string dir in _project.ScanDirectoriesRooted())
100 | {
101 | ScanDirForSourceFilesRecurse(new DirectoryInfo(dir));
102 | }
103 |
104 | feedback.Title = "Scanning headers...";
105 | foreach (string dir in _project.IncludeDirectoriesRooted())
106 | {
107 | ScanDirForHeaders(new DirectoryInfo(dir));
108 | }
109 |
110 | feedback.Title = "Scanning files...";
111 |
112 | currentPass = _nextPass.ToArray();
113 | _nextPass = [];
114 | int processedCounter = 0;
115 | while (currentPass.Length > 0)
116 | {
117 | foreach(var path in currentPass)
118 | {
119 | int current = processedCounter++;
120 | if ((current & 255) == 0)
121 | {
122 | lock (feedback)
123 | {
124 | feedback.Item = current;
125 | feedback.Count = _queued.Count;
126 | feedback.Message = Path.GetFileName(path);
127 | }
128 | }
129 | ScanFile(path);
130 | }
131 | currentPass = _nextPass.ToArray();
132 | _nextPass = [];
133 | }
134 |
135 | _queued.Clear();
136 | _system_includes.Clear();
137 |
138 | _project.ScanTime = sw.Elapsed;
139 | }
140 |
141 | void ScanDirForSourceFilesRecurse(DirectoryInfo di)
142 | {
143 | FileInfo[] files;
144 | DirectoryInfo[] subdirs;
145 |
146 | try
147 | {
148 | files = di.GetFiles();
149 | subdirs = di.GetDirectories();
150 | }
151 | catch (Exception e)
152 | {
153 | Errors.Add(new ScanError($"Failed to scan folder: {e.Message}", di.FullName));
154 | return;
155 | }
156 |
157 | foreach (FileInfo file in files)
158 | {
159 | if (SourceFile.IsTranslationUnitExtension(file.Extension))
160 | {
161 | string fullPath = file.FullName;
162 | Enqueue(fullPath, CanonicalPath(fullPath));
163 | }
164 | }
165 |
166 | foreach (DirectoryInfo subdir in subdirs)
167 | {
168 | if (!subdir.Name.StartsWith('.'))
169 | ScanDirForSourceFilesRecurse(subdir);
170 | }
171 | }
172 |
173 | void ScanDirForHeaders(DirectoryInfo di)
174 | {
175 | FileInfo[] files;
176 | try
177 | {
178 | files = di.GetFiles();
179 | }
180 | catch
181 | {
182 | // ignore exceptions
183 | return;
184 | }
185 | foreach (FileInfo file in files)
186 | {
187 | if (SourceFile.IsHeaderExtension(file.Extension))
188 | {
189 | _system_includes.TryAdd(file.Name, CanonicalPath(file.FullName));
190 | }
191 | }
192 | }
193 |
194 | // On a case-insensitive file system, this returns
195 | // path that is all lowercase
196 | string CanonicalPath(string path)
197 | {
198 | return CaseSensitive ? path : path.ToLowerInvariant();
199 | }
200 |
201 | bool FileExists(string path)
202 | {
203 | if (_file_existence.TryGetValue(path, out var value))
204 | {
205 | return value;
206 | }
207 |
208 | value = File.Exists(path);
209 | _file_existence.TryAdd(path, value);
210 | return value;
211 | }
212 |
213 | void Enqueue(string fullPath, string abs)
214 | {
215 | if (_queued.Add(abs))
216 | {
217 | _nextPass.Add(fullPath);
218 | }
219 | }
220 |
221 | bool ContainsPrecompiledPath(string abs)
222 | {
223 | return _project.Files.TryGetValue(abs, out var f) && f.Precompiled;
224 | }
225 |
226 | void ScanFile(string path)
227 | {
228 | path = CanonicalPath(path);
229 | if (_project.Files.ContainsKey(path) && !_scanning_pch)
230 | return;
231 | Parser.Result res = Parser.ParseFile(path, Errors);
232 | var sf = new SourceFile
233 | {
234 | Lines = res.Lines,
235 | LocalIncludes = res.LocalIncludes,
236 | SystemIncludes = res.SystemIncludes,
237 | Precompiled = _scanning_pch
238 | };
239 | _project.Files[path] = sf;
240 |
241 | sf.AbsoluteIncludes.Clear();
242 |
243 | string local_dir = Path.GetDirectoryName(path) ?? string.Empty;
244 | foreach (string s in sf.LocalIncludes)
245 | {
246 | try
247 | {
248 | string inc = Path.GetFullPath(Path.Combine(local_dir, s));
249 | string abs = CanonicalPath(inc);
250 | // found a header that's part of PCH during regular scan: ignore it
251 | if (!_scanning_pch && ContainsPrecompiledPath(abs))
252 | {
253 | continue;
254 | }
255 | if (!FileExists(inc))
256 | {
257 | if (!sf.SystemIncludes.Contains(s))
258 | sf.SystemIncludes.Add(s);
259 | continue;
260 | }
261 | sf.AbsoluteIncludes.Add(abs);
262 | Enqueue(inc, abs);
263 | }
264 | catch (Exception e)
265 | {
266 | Errors.Add(new ScanError($"Exception processing \"{s}\": {e.Message}", path));
267 | }
268 | }
269 |
270 | foreach (string s in sf.SystemIncludes)
271 | {
272 | try
273 | {
274 | if (_system_includes.TryGetValue(s, out var sysIncPath))
275 | {
276 | // An entry in _system_includes might have been found during the include folders
277 | // scan; does not mean that all files under it are actually included into the project yet.
278 | // Make sure it is scanned (if already is, this will early out).
279 | Enqueue(sysIncPath, sysIncPath);
280 |
281 | // found a header that's part of PCH during regular scan: ignore it
282 | if (!_scanning_pch && ContainsPrecompiledPath(sysIncPath))
283 | {
284 | continue;
285 | }
286 | sf.AbsoluteIncludes.Add(sysIncPath);
287 | }
288 | else
289 | {
290 | string found = null;
291 |
292 | foreach (string dir in _project.IncludeDirectoriesRooted())
293 | {
294 | found = Path.GetFullPath(Path.Combine(dir, s));
295 | if (FileExists(found))
296 | break;
297 | found = null;
298 | }
299 |
300 | if (found != null)
301 | {
302 | string abs = CanonicalPath(found);
303 | // found a header that's part of PCH during regular scan: ignore it
304 | if (!_scanning_pch && ContainsPrecompiledPath(abs))
305 | {
306 | continue;
307 | }
308 |
309 | sf.AbsoluteIncludes.Add(abs);
310 | _system_includes.TryAdd(s, abs);
311 | Enqueue(found, abs);
312 | }
313 | else
314 | {
315 | if (NotFound.Add(s))
316 | {
317 | NotFoundOrigins.Add(s, path);
318 | }
319 | }
320 | }
321 | }
322 | catch (Exception e)
323 | {
324 | Errors.Add(new ScanError($"Exception processing <{s}>: {e.Message}", path));
325 | }
326 | }
327 |
328 | // Only treat each include as done once. Since we completely ignore preprocessor, for patterns like
329 | // this we'd end up having same file in includes list multiple times. Let's assume that all includes use
330 | // pragma once or include guards and are only actually parsed just once.
331 | // #if FOO
332 | // #include
333 | // #else
334 | // #include
335 | // #endif
336 | sf.AbsoluteIncludes = sf.AbsoluteIncludes.Distinct().ToList();
337 | }
338 | }
--------------------------------------------------------------------------------