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
58 changes: 51 additions & 7 deletions ebuild.Tests/Unit/CompilationDatabaseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,21 +109,22 @@ public void CreateFromSettings_ShouldCreateValidEntry()
}

[Test]
public void GetEntry_WithCorruptedFile_ShouldReturnNull()
public void GetEntry_WithCorruptedDatabase_ShouldReturnNull()
{
// Arrange
var database = CompilationDatabase.Get(_testDir, "TestModule", "test.cpp");

// Create corrupted file
var dbDir = Path.Combine(_testDir, ".ebuild", "TestModule");
Directory.CreateDirectory(dbDir);
var dbFile = Path.Combine(dbDir, "test.compile.json");
File.WriteAllText(dbFile, "corrupted json content");
var dbFile = Path.Combine(dbDir, "compilation.db");

// Create corrupted database file (not a valid SQLite database)
File.WriteAllText(dbFile, "corrupted database content");

var database = CompilationDatabase.Get(_testDir, "TestModule", "test.cpp");

// Act
var entry = database.GetEntry();

// Assert
// Assert - Should return null when database is corrupted
Assert.That(entry, Is.Null);
}

Expand Down Expand Up @@ -184,4 +185,47 @@ public void RemoveEntry_WithInvalidPath_ShouldNotThrow()
// Act & Assert
Assert.DoesNotThrow(() => database.RemoveEntry());
}

[Test]
public void MultipleSourceFiles_ShouldUseSameDatabase()
{
// Arrange - Create entries for multiple source files in the same module
var database1 = CompilationDatabase.Get(_testDir, "TestModule", "file1.cpp");
var database2 = CompilationDatabase.Get(_testDir, "TestModule", "file2.cpp");

var entry1 = new CompilationEntry
{
SourceFile = "file1.cpp",
OutputFile = "file1.obj",
LastCompiled = DateTime.UtcNow,
Definitions = new List<string> { "FILE1" }
};

var entry2 = new CompilationEntry
{
SourceFile = "file2.cpp",
OutputFile = "file2.obj",
LastCompiled = DateTime.UtcNow,
Definitions = new List<string> { "FILE2" }
};

// Act - Save both entries
database1.SaveEntry(entry1);
database2.SaveEntry(entry2);

// Assert - Both entries should be retrievable
var retrieved1 = database1.GetEntry();
var retrieved2 = database2.GetEntry();

Assert.That(retrieved1, Is.Not.Null);
Assert.That(retrieved2, Is.Not.Null);
Assert.That(retrieved1!.SourceFile, Is.EqualTo("file1.cpp"));
Assert.That(retrieved2!.SourceFile, Is.EqualTo("file2.cpp"));
Assert.That(retrieved1.Definitions, Contains.Item("FILE1"));
Assert.That(retrieved2.Definitions, Contains.Item("FILE2"));

// Verify they use the same database file
var dbPath = Path.Combine(_testDir, ".ebuild", "TestModule", "compilation.db");
Assert.That(File.Exists(dbPath), Is.True, "Database file should exist");
}
}
136 changes: 107 additions & 29 deletions ebuild/Modules/BuildGraph/CompilationDatabase.cs
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using ebuild.api.Compiler;
using Microsoft.Data.Sqlite;

namespace ebuild.Modules.BuildGraph;

/// <summary>
/// Manages compilation state tracking for incremental builds
/// Manages compilation state tracking for incremental builds using SQLite
/// </summary>
public class CompilationDatabase
{
private static readonly Dictionary<string, CompilationDatabase> _dbCache = [];

#pragma warning disable IDE0052 // Fields are used for serialization
private readonly string _databasePath;
private readonly string _sourceFile;
private readonly string _moduleName;
private CompilationEntry? _cached;
#pragma warning restore IDE0052

private CompilationDatabase(string moduleDirectory, string moduleName, string sourceFile)
{
var dbDir = Path.Combine(moduleDirectory, ".ebuild", moduleName, "compdb");
var dbDir = Path.Combine(moduleDirectory, ".ebuild", moduleName);
try
{
Directory.CreateDirectory(dbDir);
Expand All @@ -32,26 +30,49 @@ private CompilationDatabase(string moduleDirectory, string moduleName, string so
// This allows the class to be constructed but operations will fail gracefully
}

var sourceFileName = Path.GetFileNameWithoutExtension(sourceFile);
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(sourceFile));
var hexHash = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
_databasePath = Path.Combine(dbDir, $"{sourceFileName}-${hexHash}.compile.json");
_databasePath = Path.Combine(dbDir, "compilation.db");
_sourceFile = sourceFile;
_moduleName = moduleName;

InitializeDatabase();
}

private void InitializeDatabase()
{
try
{
using var connection = new SqliteConnection($"Data Source={_databasePath}");
connection.Open();

var command = connection.CreateCommand();
command.CommandText = @"
CREATE TABLE IF NOT EXISTS compilation_entries (
source_file TEXT PRIMARY KEY,
output_file TEXT NOT NULL,
last_compiled TEXT NOT NULL,
definitions TEXT NOT NULL,
include_paths TEXT NOT NULL,
force_includes TEXT NOT NULL,
dependencies TEXT NOT NULL
)";
command.ExecuteNonQuery();
}
catch
{
// Ignore initialization errors - operations will fail gracefully
}
}

public static CompilationDatabase Get(string moduleDirectory, string moduleName, string sourceFile)
{
var dbDir = Path.Combine(moduleDirectory, ".ebuild", moduleName, "compdb");
var sourceFileName = Path.GetFileNameWithoutExtension(sourceFile);
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(sourceFile));
var hexHash = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
var dbPath = Path.Combine(dbDir, $"{sourceFileName}-${hexHash}.compile.json");
var dbPath = Path.Combine(moduleDirectory, ".ebuild", moduleName, "compilation.db");
var cacheKey = $"{dbPath}:{sourceFile}";
lock (_dbCache)
{
if (_dbCache.TryGetValue(dbPath, out var db))
if (_dbCache.TryGetValue(cacheKey, out var db))
return db;
db = new CompilationDatabase(moduleDirectory, moduleName, sourceFile);
_dbCache[dbPath] = db;
_dbCache[cacheKey] = db;
return db;
}
}
Expand All @@ -61,14 +82,35 @@ public static CompilationDatabase Get(string moduleDirectory, string moduleName,
if (_cached != null)
return _cached;

if (!File.Exists(_databasePath))
return null;

try
{
var json = File.ReadAllText(_databasePath);
_cached = JsonSerializer.Deserialize<CompilationEntry>(json);
return _cached;
using var connection = new SqliteConnection($"Data Source={_databasePath}");
connection.Open();

var command = connection.CreateCommand();
command.CommandText = @"
SELECT source_file, output_file, last_compiled, definitions, include_paths, force_includes, dependencies
FROM compilation_entries
WHERE source_file = $sourceFile";
command.Parameters.AddWithValue("$sourceFile", _sourceFile);

using var reader = command.ExecuteReader();
if (reader.Read())
{
_cached = new CompilationEntry
{
SourceFile = reader.GetString(0),
OutputFile = reader.GetString(1),
LastCompiled = DateTime.Parse(reader.GetString(2)),
Definitions = DeserializeList(reader.GetString(3)),
IncludePaths = DeserializeList(reader.GetString(4)),
ForceIncludes = DeserializeList(reader.GetString(5)),
Dependencies = DeserializeList(reader.GetString(6))
};
return _cached;
}

return null;
}
catch
{
Expand All @@ -80,8 +122,24 @@ public void SaveEntry(CompilationEntry entry)
{
try
{
var json = JsonSerializer.Serialize(entry, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(_databasePath, json);
using var connection = new SqliteConnection($"Data Source={_databasePath}");
connection.Open();

var command = connection.CreateCommand();
command.CommandText = @"
INSERT OR REPLACE INTO compilation_entries
(source_file, output_file, last_compiled, definitions, include_paths, force_includes, dependencies)
VALUES ($sourceFile, $outputFile, $lastCompiled, $definitions, $includePaths, $forceIncludes, $dependencies)";

command.Parameters.AddWithValue("$sourceFile", entry.SourceFile);
command.Parameters.AddWithValue("$outputFile", entry.OutputFile);
command.Parameters.AddWithValue("$lastCompiled", entry.LastCompiled.ToString("o"));
command.Parameters.AddWithValue("$definitions", SerializeList(entry.Definitions));
command.Parameters.AddWithValue("$includePaths", SerializeList(entry.IncludePaths));
command.Parameters.AddWithValue("$forceIncludes", SerializeList(entry.ForceIncludes));
command.Parameters.AddWithValue("$dependencies", SerializeList(entry.Dependencies));

command.ExecuteNonQuery();
_cached = entry;
}
catch
Expand All @@ -94,10 +152,14 @@ public void RemoveEntry()
{
try
{
if (File.Exists(_databasePath))
{
File.Delete(_databasePath);
}
using var connection = new SqliteConnection($"Data Source={_databasePath}");
connection.Open();

var command = connection.CreateCommand();
command.CommandText = "DELETE FROM compilation_entries WHERE source_file = $sourceFile";
command.Parameters.AddWithValue("$sourceFile", _sourceFile);
command.ExecuteNonQuery();

_cached = null;
}
catch
Expand All @@ -106,6 +168,22 @@ public void RemoveEntry()
}
}

private static string SerializeList(List<string> list)
{
return string.Join("\n", list.Select(s => Convert.ToBase64String(Encoding.UTF8.GetBytes(s))));
}

private static List<string> DeserializeList(string serialized)
{
if (string.IsNullOrEmpty(serialized))
return new List<string>();

return serialized.Split('\n')
.Where(s => !string.IsNullOrEmpty(s))
.Select(s => Encoding.UTF8.GetString(Convert.FromBase64String(s)))
.ToList();
}

public static CompilationEntry CreateFromSettings(CompilerSettings settings, string outputFile)
{
return new CompilationEntry
Expand Down
1 change: 1 addition & 0 deletions ebuild/ebuild.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CliFx" Version="2.3.6" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
Expand Down
Loading