diff --git a/src/AXSharp.compiler/src/AXSharp.Compiler.Abstractions/ICompilerOptions.cs b/src/AXSharp.compiler/src/AXSharp.Compiler.Abstractions/ICompilerOptions.cs
index 07ca8c91..2e00297b 100644
--- a/src/AXSharp.compiler/src/AXSharp.Compiler.Abstractions/ICompilerOptions.cs
+++ b/src/AXSharp.compiler/src/AXSharp.Compiler.Abstractions/ICompilerOptions.cs
@@ -31,4 +31,12 @@ public interface ICompilerOptions
///
string? UiHostProject { get; set; }
+ ///
+ /// Source-origin detection mode controlling the provenance attributes emitted onto generated
+ /// twins: auto (detect a git repository with a remote, otherwise fall back to apax
+ /// package identity), apax (force apax package mode, deterministic, no commit SHA), or
+ /// off (legacy: src-relative paths and no assembly-level attribute).
+ ///
+ string SourceOrigin { get; set; }
+
}
\ No newline at end of file
diff --git a/src/AXSharp.compiler/src/AXSharp.Compiler/AXSharp.Compiler.csproj b/src/AXSharp.compiler/src/AXSharp.Compiler/AXSharp.Compiler.csproj
index 693c4748..86254c6c 100644
--- a/src/AXSharp.compiler/src/AXSharp.Compiler/AXSharp.Compiler.csproj
+++ b/src/AXSharp.compiler/src/AXSharp.Compiler/AXSharp.Compiler.csproj
@@ -61,7 +61,8 @@
-
+
+
diff --git a/src/AXSharp.compiler/src/AXSharp.Compiler/AXSharpConfig.cs b/src/AXSharp.compiler/src/AXSharp.Compiler/AXSharpConfig.cs
index 50d060a8..0bd38ed5 100644
--- a/src/AXSharp.compiler/src/AXSharp.Compiler/AXSharpConfig.cs
+++ b/src/AXSharp.compiler/src/AXSharp.Compiler/AXSharpConfig.cs
@@ -70,6 +70,18 @@ public string OutputProjectFolder
///
public string? UiHostProject { get; set; }
+ private string _sourceOrigin = "auto";
+
+ ///
+ /// Source-origin detection mode controlling provenance attributes emitted onto generated twins:
+ /// auto (default), apax or off.
+ ///
+ public string SourceOrigin
+ {
+ get => string.IsNullOrWhiteSpace(_sourceOrigin) ? "auto" : _sourceOrigin;
+ set => _sourceOrigin = value;
+ }
+
private string _axProjectFolder;
@@ -211,5 +223,8 @@ private static void OverridesFromCli(ICompilerOptions fromConfig, ICompilerOptio
fromConfig.UiHostProject = string.IsNullOrEmpty(newCompilerOptions.UiHostProject)
? fromConfig.UiHostProject
: newCompilerOptions.UiHostProject;
+ fromConfig.SourceOrigin = string.IsNullOrEmpty(newCompilerOptions.SourceOrigin)
+ ? fromConfig.SourceOrigin
+ : newCompilerOptions.SourceOrigin;
}
}
\ No newline at end of file
diff --git a/src/AXSharp.compiler/src/AXSharp.Compiler/AXSharpProject.cs b/src/AXSharp.compiler/src/AXSharp.Compiler/AXSharpProject.cs
index bac20568..2a847ac7 100644
--- a/src/AXSharp.compiler/src/AXSharp.Compiler/AXSharpProject.cs
+++ b/src/AXSharp.compiler/src/AXSharp.Compiler/AXSharpProject.cs
@@ -42,10 +42,11 @@ public class AXSharpProject : IAXSharpProject
///
/// Compiler options from CLI.
///
- public AXSharpProject(AxProject axProject, IEnumerable builderTypes, Type targetProjectType, ICompilerOptions? cliCompilerOptions = null, ICompilerOptions? dependnantCompilerOptions = null)
+ public AXSharpProject(AxProject axProject, IEnumerable builderTypes, Type targetProjectType, ICompilerOptions? cliCompilerOptions = null, ICompilerOptions? dependnantCompilerOptions = null, ISourceOriginProvider? sourceOriginProvider = null)
{
AxProject = axProject;
CompilerOptions = AXSharpConfig.UpdateAndGetAXSharpConfig(axProject.ProjectFolder, cliCompilerOptions, dependnantCompilerOptions);
+ _sourceOriginProvider = sourceOriginProvider ?? SourceOriginProviderFactory.Create(CompilerOptions?.SourceOrigin);
if (CompilerOptions != null)
{
if(string.IsNullOrEmpty(CompilerOptions.OutputProjectFolder))
@@ -76,6 +77,16 @@ public AXSharpProject(AxProject axProject, IEnumerable builderTypes, Type
///
public AxProject AxProject { get; }
+ private readonly ISourceOriginProvider _sourceOriginProvider;
+ private SourceOrigin? _sourceOrigin;
+
+ ///
+ /// Gets the resolved (repository or apax-package provenance) for
+ /// this project. Determines the base folder for per-type source paths and which assembly-level
+ /// attribute is emitted. Resolved lazily and cached.
+ ///
+ public SourceOrigin SourceOrigin => _sourceOrigin ??= _sourceOriginProvider.Resolve(AxProject);
+
private IEnumerable BuilderTypes { get; }
///
@@ -201,6 +212,8 @@ public void Generate()
}
}
+ EmitAssemblyAttributes();
+
//TargetProject.ProvisionProjectStructure();
GenerateMetadata(compilationResult.Compilation, projectSources);
TargetProject.GenerateResources();
@@ -208,6 +221,31 @@ public void Generate()
Log.Logger.Information($"Compilation of project '{AxProject.SrcFolder}' done.");
}
+ ///
+ /// Emits the single assembly-level provenance attribute (.g/AssemblyAttributes.g.cs)
+ /// describing the source origin of this project. No file is written in None mode.
+ ///
+ private void EmitAssemblyAttributes()
+ {
+ var assemblyAttribute = SourceOriginEmitter.RenderAssemblyAttribute(SourceOrigin);
+ if (assemblyAttribute == null)
+ return;
+
+ Policy
+ .Handle()
+ .WaitAndRetry(5, a => TimeSpan.FromMilliseconds(500))
+ .Execute(() =>
+ {
+ using (var swr = new StreamWriter(Path.Combine(
+ EnsureFolder(Path.Combine(OutputFolder, ".g")),
+ "AssemblyAttributes.g.cs")))
+ {
+ swr.WriteLine("// ");
+ swr.WriteLine(assemblyAttribute);
+ }
+ });
+ }
+
///
diff --git a/src/AXSharp.compiler/src/AXSharp.Compiler/SourceOrigin/ApaxSourceOriginProvider.cs b/src/AXSharp.compiler/src/AXSharp.Compiler/SourceOrigin/ApaxSourceOriginProvider.cs
new file mode 100644
index 00000000..7faceea4
--- /dev/null
+++ b/src/AXSharp.compiler/src/AXSharp.Compiler/SourceOrigin/ApaxSourceOriginProvider.cs
@@ -0,0 +1,22 @@
+// AXSharp.Compiler
+// Copyright (c) 2023 MTS spol. s r.o., and Contributors. All Rights Reserved.
+// Contributors: https://github.com/inxton/axsharp/graphs/contributors
+// See the LICENSE file in the repository root for more information.
+// https://github.com/inxton/axsharp/blob/dev/LICENSE
+// Third party licenses: https://github.com/inxton/axsharp/blob/master/notices.md
+
+namespace AXSharp.Compiler;
+
+///
+/// Resolves Library-mode provenance from the project's apax metadata. Per-type paths are rooted
+/// at the project src folder. Missing name/version map to empty strings.
+///
+public sealed class ApaxSourceOriginProvider : ISourceOriginProvider
+{
+ ///
+ public SourceOrigin Resolve(AxProject project)
+ => SourceOrigin.Library(
+ project.SrcFolder,
+ project.ProjectInfo.Name ?? string.Empty,
+ project.ProjectInfo.Version ?? string.Empty);
+}
diff --git a/src/AXSharp.compiler/src/AXSharp.Compiler/SourceOrigin/GitSourceOriginProvider.cs b/src/AXSharp.compiler/src/AXSharp.Compiler/SourceOrigin/GitSourceOriginProvider.cs
new file mode 100644
index 00000000..14219997
--- /dev/null
+++ b/src/AXSharp.compiler/src/AXSharp.Compiler/SourceOrigin/GitSourceOriginProvider.cs
@@ -0,0 +1,50 @@
+// AXSharp.Compiler
+// Copyright (c) 2023 MTS spol. s r.o., and Contributors. All Rights Reserved.
+// Contributors: https://github.com/inxton/axsharp/graphs/contributors
+// See the LICENSE file in the repository root for more information.
+// https://github.com/inxton/axsharp/blob/dev/LICENSE
+// Third party licenses: https://github.com/inxton/axsharp/blob/master/notices.md
+
+using LibGit2Sharp;
+
+namespace AXSharp.Compiler;
+
+///
+/// Resolves Repository-mode provenance from the nearest enclosing git repository (innermost
+/// .git — submodules/worktrees resolve to their own repository) that exposes an
+/// origin remote. The per-type path base is the repository working directory; the remote
+/// URL is normalized to canonical https. When no repository or no usable origin remote is
+/// found, resolution delegates to the fallback (apax) provider.
+///
+public sealed class GitSourceOriginProvider : ISourceOriginProvider
+{
+ private readonly ISourceOriginProvider _fallback;
+
+ ///
+ /// Creates a new instance. is used when the project is not in a
+ /// git repository with a usable origin remote (defaults to ).
+ ///
+ public GitSourceOriginProvider(ISourceOriginProvider? fallback = null)
+ => _fallback = fallback ?? new ApaxSourceOriginProvider();
+
+ ///
+ public SourceOrigin Resolve(AxProject project)
+ {
+ var gitDir = Repository.Discover(project.ProjectFolder);
+ if (string.IsNullOrEmpty(gitDir))
+ return _fallback.Resolve(project);
+
+ using var repo = new Repository(gitDir);
+
+ var origin = repo.Network.Remotes["origin"];
+ if (origin == null || string.IsNullOrWhiteSpace(origin.Url))
+ return _fallback.Resolve(project);
+
+ var url = GitUrlNormalizer.Normalize(origin.Url);
+ var commit = repo.Head?.Tip?.Sha ?? string.Empty;
+ var branch = repo.Info.IsHeadDetached ? string.Empty : repo.Head?.FriendlyName ?? string.Empty;
+ var repoRoot = repo.Info.WorkingDirectory ?? project.ProjectFolder;
+
+ return SourceOrigin.Repository(repoRoot, url, commit, branch);
+ }
+}
diff --git a/src/AXSharp.compiler/src/AXSharp.Compiler/SourceOrigin/GitUrlNormalizer.cs b/src/AXSharp.compiler/src/AXSharp.Compiler/SourceOrigin/GitUrlNormalizer.cs
new file mode 100644
index 00000000..6ca12e1b
--- /dev/null
+++ b/src/AXSharp.compiler/src/AXSharp.Compiler/SourceOrigin/GitUrlNormalizer.cs
@@ -0,0 +1,56 @@
+// AXSharp.Compiler
+// Copyright (c) 2023 MTS spol. s r.o., and Contributors. All Rights Reserved.
+// Contributors: https://github.com/inxton/axsharp/graphs/contributors
+// See the LICENSE file in the repository root for more information.
+// https://github.com/inxton/axsharp/blob/dev/LICENSE
+// Third party licenses: https://github.com/inxton/axsharp/blob/master/notices.md
+
+using System;
+using System.Text.RegularExpressions;
+
+namespace AXSharp.Compiler;
+
+///
+/// Normalizes a git remote URL into a canonical, credential-free https form suitable for
+/// embedding in AXSharp.Connector.SourceRepositoryAttribute and for building source
+/// permalinks. Best-effort: input that is not recognizable as a remote URL is returned unchanged.
+///
+public static class GitUrlNormalizer
+{
+ // scp-like syntax: [user@]host:org/repo[.git] (no URI scheme)
+ private static readonly Regex ScpLike = new(@"^(?:[^@/]+@)?([^/:]+):(.+)$", RegexOptions.Compiled);
+
+ ///
+ /// Normalizes to canonical https, stripping any embedded
+ /// credentials, port and trailing .git/slash. Returns for
+ /// null/blank input and the trimmed input verbatim when it is not a recognizable remote URL.
+ ///
+ public static string Normalize(string? raw)
+ {
+ if (string.IsNullOrWhiteSpace(raw))
+ return string.Empty;
+
+ var url = raw.Trim();
+
+ if (!url.Contains("://"))
+ {
+ var scp = ScpLike.Match(url);
+ return scp.Success
+ ? BuildHttps(scp.Groups[1].Value, scp.Groups[2].Value)
+ : url; // not a remote URL — leave untouched
+ }
+
+ return Uri.TryCreate(url, UriKind.Absolute, out var uri)
+ ? BuildHttps(uri.Host, uri.AbsolutePath)
+ : url;
+ }
+
+ private static string BuildHttps(string host, string path)
+ {
+ path = path.Trim('/');
+ if (path.EndsWith(".git", StringComparison.OrdinalIgnoreCase))
+ path = path.Substring(0, path.Length - 4);
+
+ return $"https://{host}/{path}";
+ }
+}
diff --git a/src/AXSharp.compiler/src/AXSharp.Compiler/SourceOrigin/ISourceOriginProvider.cs b/src/AXSharp.compiler/src/AXSharp.Compiler/SourceOrigin/ISourceOriginProvider.cs
new file mode 100644
index 00000000..2cb70f90
--- /dev/null
+++ b/src/AXSharp.compiler/src/AXSharp.Compiler/SourceOrigin/ISourceOriginProvider.cs
@@ -0,0 +1,18 @@
+// AXSharp.Compiler
+// Copyright (c) 2023 MTS spol. s r.o., and Contributors. All Rights Reserved.
+// Contributors: https://github.com/inxton/axsharp/graphs/contributors
+// See the LICENSE file in the repository root for more information.
+// https://github.com/inxton/axsharp/blob/dev/LICENSE
+// Third party licenses: https://github.com/inxton/axsharp/blob/master/notices.md
+
+namespace AXSharp.Compiler;
+
+///
+/// Resolves the (repository or apax-package provenance) for an
+/// AX project. Implementations are the seam that lets tests force deterministic provenance.
+///
+public interface ISourceOriginProvider
+{
+ /// Resolves the provenance for .
+ SourceOrigin Resolve(AxProject project);
+}
diff --git a/src/AXSharp.compiler/src/AXSharp.Compiler/SourceOrigin/NoSourceOriginProvider.cs b/src/AXSharp.compiler/src/AXSharp.Compiler/SourceOrigin/NoSourceOriginProvider.cs
new file mode 100644
index 00000000..5990a7e9
--- /dev/null
+++ b/src/AXSharp.compiler/src/AXSharp.Compiler/SourceOrigin/NoSourceOriginProvider.cs
@@ -0,0 +1,18 @@
+// AXSharp.Compiler
+// Copyright (c) 2023 MTS spol. s r.o., and Contributors. All Rights Reserved.
+// Contributors: https://github.com/inxton/axsharp/graphs/contributors
+// See the LICENSE file in the repository root for more information.
+// https://github.com/inxton/axsharp/blob/dev/LICENSE
+// Third party licenses: https://github.com/inxton/axsharp/blob/master/notices.md
+
+namespace AXSharp.Compiler;
+
+///
+/// Resolves None-mode provenance: no assembly-level attribute is emitted and per-type paths stay
+/// rooted at the project src folder. This reproduces the pre-feature (legacy) behavior.
+///
+public sealed class NoSourceOriginProvider : ISourceOriginProvider
+{
+ ///
+ public SourceOrigin Resolve(AxProject project) => SourceOrigin.None(project.SrcFolder);
+}
diff --git a/src/AXSharp.compiler/src/AXSharp.Compiler/SourceOrigin/SourceOrigin.cs b/src/AXSharp.compiler/src/AXSharp.Compiler/SourceOrigin/SourceOrigin.cs
new file mode 100644
index 00000000..cf0a513a
--- /dev/null
+++ b/src/AXSharp.compiler/src/AXSharp.Compiler/SourceOrigin/SourceOrigin.cs
@@ -0,0 +1,82 @@
+// AXSharp.Compiler
+// Copyright (c) 2023 MTS spol. s r.o., and Contributors. All Rights Reserved.
+// Contributors: https://github.com/inxton/axsharp/graphs/contributors
+// See the LICENSE file in the repository root for more information.
+// https://github.com/inxton/axsharp/blob/dev/LICENSE
+// Third party licenses: https://github.com/inxton/axsharp/blob/master/notices.md
+
+namespace AXSharp.Compiler;
+
+///
+/// The kind of provenance resolved for a transpiled assembly. Determines which assembly-level
+/// attribute is emitted and how the per-type source path is rooted.
+///
+public enum SourceOriginMode
+{
+ /// Git repository with a usable remote; per-type path is repository-root relative.
+ Repository,
+
+ /// apax package; per-type path is src-folder relative.
+ Library,
+
+ /// No provenance emitted; per-type path is src-folder relative (legacy).
+ None
+}
+
+///
+/// Resolved provenance of a transpiled AX project: the base folder against which per-type source
+/// paths are relativized, plus the repository or package identity to emit at assembly level.
+///
+public sealed class SourceOrigin
+{
+ private SourceOrigin(
+ SourceOriginMode mode,
+ string baseFolder,
+ string? url,
+ string? commit,
+ string? branch,
+ string? packageName,
+ string? packageVersion)
+ {
+ Mode = mode;
+ BaseFolder = baseFolder;
+ Url = url;
+ Commit = commit;
+ Branch = branch;
+ PackageName = packageName;
+ PackageVersion = packageVersion;
+ }
+
+ /// Gets the resolved provenance mode.
+ public SourceOriginMode Mode { get; }
+
+ /// Gets the folder against which per-type source paths are relativized.
+ public string BaseFolder { get; }
+
+ /// Gets the canonical remote URL (Repository mode); otherwise null.
+ public string? Url { get; }
+
+ /// Gets the commit SHA (Repository mode); otherwise null.
+ public string? Commit { get; }
+
+ /// Gets the branch name, empty on detached HEAD (Repository mode); otherwise null.
+ public string? Branch { get; }
+
+ /// Gets the apax package name (Library mode); otherwise null.
+ public string? PackageName { get; }
+
+ /// Gets the apax package version (Library mode); otherwise null.
+ public string? PackageVersion { get; }
+
+ /// Creates a Repository-mode origin rooted at the repository working directory.
+ public static SourceOrigin Repository(string baseFolder, string url, string commit, string branch)
+ => new(SourceOriginMode.Repository, baseFolder, url, commit, branch, null, null);
+
+ /// Creates a Library-mode origin rooted at the project src folder.
+ public static SourceOrigin Library(string baseFolder, string name, string version)
+ => new(SourceOriginMode.Library, baseFolder, null, null, null, name, version);
+
+ /// Creates a None-mode origin rooted at the project src folder (legacy, no emission).
+ public static SourceOrigin None(string baseFolder)
+ => new(SourceOriginMode.None, baseFolder, null, null, null, null, null);
+}
diff --git a/src/AXSharp.compiler/src/AXSharp.Compiler/SourceOrigin/SourceOriginEmitter.cs b/src/AXSharp.compiler/src/AXSharp.Compiler/SourceOrigin/SourceOriginEmitter.cs
new file mode 100644
index 00000000..11d96cc2
--- /dev/null
+++ b/src/AXSharp.compiler/src/AXSharp.Compiler/SourceOrigin/SourceOriginEmitter.cs
@@ -0,0 +1,36 @@
+// AXSharp.Compiler
+// Copyright (c) 2023 MTS spol. s r.o., and Contributors. All Rights Reserved.
+// Contributors: https://github.com/inxton/axsharp/graphs/contributors
+// See the LICENSE file in the repository root for more information.
+// https://github.com/inxton/axsharp/blob/dev/LICENSE
+// Third party licenses: https://github.com/inxton/axsharp/blob/master/notices.md
+
+namespace AXSharp.Compiler;
+
+///
+/// Renders the single assembly-level provenance attribute for a resolved .
+///
+public static class SourceOriginEmitter
+{
+ ///
+ /// Returns the [assembly: ...] declaration for :
+ /// AXSharp.Connector.SourceRepository in repository mode,
+ /// AXSharp.Connector.SourceLibrary in library mode, or null in None mode
+ /// (no attribute is emitted).
+ ///
+ public static string? RenderAssemblyAttribute(SourceOrigin origin)
+ {
+ switch (origin.Mode)
+ {
+ case SourceOriginMode.Repository:
+ return $"[assembly: AXSharp.Connector.SourceRepository({Verbatim(origin.Url)}, {Verbatim(origin.Commit)}, {Verbatim(origin.Branch)})]";
+ case SourceOriginMode.Library:
+ return $"[assembly: AXSharp.Connector.SourceLibrary({Verbatim(origin.PackageName)}, {Verbatim(origin.PackageVersion)})]";
+ default:
+ return null;
+ }
+ }
+
+ private static string Verbatim(string? value)
+ => "@\"" + (value ?? string.Empty).Replace("\"", "\"\"") + "\"";
+}
diff --git a/src/AXSharp.compiler/src/AXSharp.Compiler/SourceOrigin/SourceOriginProviderFactory.cs b/src/AXSharp.compiler/src/AXSharp.Compiler/SourceOrigin/SourceOriginProviderFactory.cs
new file mode 100644
index 00000000..aa62e615
--- /dev/null
+++ b/src/AXSharp.compiler/src/AXSharp.Compiler/SourceOrigin/SourceOriginProviderFactory.cs
@@ -0,0 +1,25 @@
+// AXSharp.Compiler
+// Copyright (c) 2023 MTS spol. s r.o., and Contributors. All Rights Reserved.
+// Contributors: https://github.com/inxton/axsharp/graphs/contributors
+// See the LICENSE file in the repository root for more information.
+// https://github.com/inxton/axsharp/blob/dev/LICENSE
+// Third party licenses: https://github.com/inxton/axsharp/blob/master/notices.md
+
+namespace AXSharp.Compiler;
+
+///
+/// Maps the --source-origin option value to an :
+/// apax forces package mode, off disables emission, anything else (including
+/// auto, null or unrecognized) selects git auto-detection.
+///
+public static class SourceOriginProviderFactory
+{
+ /// Creates the provider for the given mode (case-insensitive).
+ public static ISourceOriginProvider Create(string? mode)
+ => (mode ?? string.Empty).Trim().ToLowerInvariant() switch
+ {
+ "apax" => new ApaxSourceOriginProvider(),
+ "off" => new NoSourceOriginProvider(),
+ _ => new GitSourceOriginProvider()
+ };
+}
diff --git a/src/AXSharp.compiler/src/AXSharp.Cs.Compiler/Helpers/SourceFileAttributeHelper.cs b/src/AXSharp.compiler/src/AXSharp.Cs.Compiler/Helpers/SourceFileAttributeHelper.cs
index 4283dcc6..4fb7a41d 100644
--- a/src/AXSharp.compiler/src/AXSharp.Cs.Compiler/Helpers/SourceFileAttributeHelper.cs
+++ b/src/AXSharp.compiler/src/AXSharp.Cs.Compiler/Helpers/SourceFileAttributeHelper.cs
@@ -20,17 +20,18 @@ public static class SourceFileAttributeHelper
{
///
/// Produces the [AXSharp.Connector.SourceFileAttribute(...)] declaration for the given type,
- /// carrying the source file path relative to the project src folder (forward-slash separated).
+ /// carrying the source file path relative to (the repository
+ /// root in repository mode, otherwise the project src folder), forward-slash separated.
/// Returns an empty string when the declaration has no source location (e.g. types parsed from
/// dependency metadata), making emission a safe no-op.
///
- public static string GetSourceFileAttribute(this ITypeDeclaration declaration, AxProject axProject)
+ public static string GetSourceFileAttribute(this ITypeDeclaration declaration, SourceOrigin sourceOrigin)
{
var filename = declaration.Location?.GetLineSpan().Filename;
if (string.IsNullOrEmpty(filename))
return string.Empty;
- var relative = Path.GetRelativePath(axProject.SrcFolder, filename).Replace('\\', '/');
+ var relative = Path.GetRelativePath(sourceOrigin.BaseFolder, filename).Replace('\\', '/');
return $"[AXSharp.Connector.SourceFileAttribute(@\"{relative}\")]\n";
}
}
diff --git a/src/AXSharp.compiler/src/AXSharp.Cs.Compiler/Onliner/CsOnlinerSourceBuilder.cs b/src/AXSharp.compiler/src/AXSharp.Cs.Compiler/Onliner/CsOnlinerSourceBuilder.cs
index 516a090a..06cc7a07 100644
--- a/src/AXSharp.compiler/src/AXSharp.Cs.Compiler/Onliner/CsOnlinerSourceBuilder.cs
+++ b/src/AXSharp.compiler/src/AXSharp.Cs.Compiler/Onliner/CsOnlinerSourceBuilder.cs
@@ -151,7 +151,7 @@ public void CreateClassDeclaration(IClassDeclarationSyntax classDeclarationSynta
classDeclarationSyntax.UsingDirectives.ToList().ForEach(p => p.Visit(visitor, this));
var generic = classDeclaration.GetGenericAttributes();
- AddToSource(classDeclaration.GetSourceFileAttribute(Project.AxProject));
+ AddToSource(classDeclaration.GetSourceFileAttribute(Project.SourceOrigin));
AddToSource(classDeclaration.Pragmas.AddAttributes());
AddToSource($"{classDeclaration.AccessModifier.Transform()}partial class {classDeclaration.Name}{generic?.Product}");
AddToSource(":");
@@ -288,7 +288,7 @@ public void CreateEnumTypeDeclaration(IEnumTypeDeclarationSyntax enumTypeDeclara
{
TypeCommAccessibility = eCommAccessibility.None;
- AddToSource(typeDeclaration.GetSourceFileAttribute(Project.AxProject));
+ AddToSource(typeDeclaration.GetSourceFileAttribute(Project.SourceOrigin));
AddToSource($"public enum {enumTypeDeclarationSyntax.Name.Text} {{");
AddToSource(string.Join("\n,", enumTypeDeclarationSyntax.EnumValueList.EnumValues.Select(p => p.Name.Text)));
AddToSource("}");
@@ -300,7 +300,7 @@ public void CreateNamedValueTypeDeclaration(INamedValueTypeDeclarationSyntax nam
{
TypeCommAccessibility = eCommAccessibility.None;
- AddToSource(namedValueTypeDeclaration.GetSourceFileAttribute(Project.AxProject));
+ AddToSource(namedValueTypeDeclaration.GetSourceFileAttribute(Project.SourceOrigin));
AddToSource(
$"public enum {namedValueTypeDeclarationSyntax.Name.Text} : {namedValueTypeDeclarationSyntax.BaseType.TransformType()} {{");
@@ -352,7 +352,7 @@ public void CreateInterfaceDeclaration(IInterfaceDeclarationSyntax interfaceDecl
{
TypeCommAccessibility = eCommAccessibility.None;
- AddToSource(interfaceDeclaration.GetSourceFileAttribute(Project.AxProject));
+ AddToSource(interfaceDeclaration.GetSourceFileAttribute(Project.SourceOrigin));
AddToSource($"{interfaceDeclaration.AccessModifier.Transform()} partial interface {interfaceDeclaration.Name} {{}}");
}
@@ -369,7 +369,7 @@ public void CreateStructuredType(IStructTypeDeclarationSyntax structTypeDeclarat
{
TypeCommAccessibility = structuredTypeDeclaration.GetCommAccessibility(this);
- AddToSource(structuredTypeDeclaration.GetSourceFileAttribute(Project.AxProject));
+ AddToSource(structuredTypeDeclaration.GetSourceFileAttribute(Project.SourceOrigin));
AddToSource(structuredTypeDeclaration.Pragmas.AddAttributes());
AddToSource(
$"{structuredTypeDeclaration.AccessModifier.Transform()}partial class {structTypeDeclarationSyntax.Name.Text}");
diff --git a/src/AXSharp.compiler/src/AXSharp.Cs.Compiler/Plain/CsPlainSourceBuilder.cs b/src/AXSharp.compiler/src/AXSharp.Cs.Compiler/Plain/CsPlainSourceBuilder.cs
index 3d05bffc..a6fc9d2d 100644
--- a/src/AXSharp.compiler/src/AXSharp.Cs.Compiler/Plain/CsPlainSourceBuilder.cs
+++ b/src/AXSharp.compiler/src/AXSharp.Cs.Compiler/Plain/CsPlainSourceBuilder.cs
@@ -71,7 +71,7 @@ public void CreateClassDeclaration(IClassDeclarationSyntax classDeclarationSynta
var classDeclarations = this.Compilation.GetSemanticTree().Classes
.Where(p => p.FullyQualifiedName == classDeclaration.GetQualifiedName());
- AddToSource(classDeclaration.GetSourceFileAttribute(Project.AxProject));
+ AddToSource(classDeclaration.GetSourceFileAttribute(Project.SourceOrigin));
AddToSource(classDeclaration.Pragmas.AddedPropertiesAsAttributes());
AddToSource($"{classDeclaration.AccessModifier.Transform()}partial class {classDeclaration.Name}");
@@ -269,7 +269,7 @@ public void CreateInterfaceDeclaration(IInterfaceDeclarationSyntax interfaceDecl
IInterfaceDeclaration interfaceDeclaration,
IxNodeVisitor visitor)
{
- AddToSource(interfaceDeclaration.GetSourceFileAttribute(Project.AxProject));
+ AddToSource(interfaceDeclaration.GetSourceFileAttribute(Project.SourceOrigin));
AddToSource($"{interfaceDeclaration.AccessModifier.Transform()} partial interface {interfaceDeclaration.Name} {{}}");
}
@@ -351,7 +351,7 @@ public void CreateStructuredType(IStructTypeDeclarationSyntax structTypeDeclarat
{
TypeCommAccessibility = structuredTypeDeclaration.GetCommAccessibility(this);
- AddToSource(structuredTypeDeclaration.GetSourceFileAttribute(Project.AxProject));
+ AddToSource(structuredTypeDeclaration.GetSourceFileAttribute(Project.SourceOrigin));
AddToSource(
$"{structuredTypeDeclaration.AccessModifier.Transform()}partial class {structTypeDeclarationSyntax.Name.Text} : AXSharp.Connector.IPlain");
AddToSource("{");
diff --git a/src/AXSharp.compiler/src/ixc/Options.cs b/src/AXSharp.compiler/src/ixc/Options.cs
index 55066b79..a9f55eb3 100644
--- a/src/AXSharp.compiler/src/ixc/Options.cs
+++ b/src/AXSharp.compiler/src/ixc/Options.cs
@@ -55,5 +55,9 @@ internal class Options : ICompilerOptions
HelpText = "Path (relative to AX project folder or absolute) of the .csproj that hosts/consumes UI companion NuGet packages. In library development this is the Blazor/UI application; in application development this is the application project itself.")]
public string? UiHostProject { get; set; }
+ [Option("source-origin", Required = false, Default = "auto",
+ HelpText = "Provenance emitted onto generated twins: 'auto' (detect git repository + remote, else apax package identity), 'apax' (force apax package mode, no commit SHA), or 'off' (legacy: src-relative paths, no assembly attribute).")]
+ public string SourceOrigin { get; set; } = "auto";
+
}
diff --git a/src/AXSharp.compiler/src/ixd/Options.cs b/src/AXSharp.compiler/src/ixd/Options.cs
index fd60b7d0..a5c352d5 100644
--- a/src/AXSharp.compiler/src/ixd/Options.cs
+++ b/src/AXSharp.compiler/src/ixd/Options.cs
@@ -39,5 +39,7 @@ internal class Options : ICompilerOptions
public string TargetPlatfromMoniker { get; set; }
public string? UiHostProject { get; set; }
+
+ public string SourceOrigin { get; set; } = "auto";
}
}
diff --git a/src/AXSharp.compiler/src/ixr/Options.cs b/src/AXSharp.compiler/src/ixr/Options.cs
index 67b42c5b..c9449a83 100644
--- a/src/AXSharp.compiler/src/ixr/Options.cs
+++ b/src/AXSharp.compiler/src/ixr/Options.cs
@@ -38,5 +38,7 @@ internal class Options : ICompilerOptions
public string TargetPlatfromMoniker { get; set; }
public string? UiHostProject { get; set; }
+
+ public string SourceOrigin { get; set; } = "auto";
}
}
diff --git a/src/AXSharp.compiler/tests/AXSharp.Compiler.CsTests/AssemblyAttributesEmissionTests.cs b/src/AXSharp.compiler/tests/AXSharp.Compiler.CsTests/AssemblyAttributesEmissionTests.cs
new file mode 100644
index 00000000..f8be9cba
--- /dev/null
+++ b/src/AXSharp.compiler/tests/AXSharp.Compiler.CsTests/AssemblyAttributesEmissionTests.cs
@@ -0,0 +1,100 @@
+// AXSharp.Compiler.CsTests
+// Copyright (c) 2023 MTS spol. s r.o., and Contributors. All Rights Reserved.
+// Contributors: https://github.com/inxton/axsharp/graphs/contributors
+// See the LICENSE file in the repository root for more information.
+// https://github.com/inxton/axsharp/blob/dev/LICENSE
+// Third party licenses: https://github.com/inxton/axsharp/blob/master/notices.md
+
+using System;
+using System.IO;
+using System.Reflection;
+using AXSharp.Compiler;
+using AXSharp.Compiler.Cs;
+using AXSharp.Compiler.Cs.Onliner;
+using Polly;
+using Xunit;
+
+namespace AXSharp.Compiler.CsTests;
+
+public class AssemblyAttributesEmissionTests
+{
+ private readonly string testFolder;
+
+ public AssemblyAttributesEmissionTests()
+ {
+#pragma warning disable CS8604
+ var executingAssemblyFileInfo = new FileInfo(Assembly.GetExecutingAssembly().FullName);
+#pragma warning restore CS8604
+ testFolder = executingAssemblyFileInfo.Directory!.FullName;
+ }
+
+ private sealed class FixedOriginProvider : ISourceOriginProvider
+ {
+ private readonly SourceOrigin _origin;
+ public FixedOriginProvider(SourceOrigin origin) => _origin = origin;
+ public SourceOrigin Resolve(AxProject project) => _origin;
+ }
+
+ [Fact]
+ public void Library_mode_emits_SourceLibrary_assembly_attribute()
+ {
+ // sourcefile sample is "@ax/sourcefile" @ 0.0.0
+ var content = GenerateAssemblyAttributes(new ApaxSourceOriginProvider(), "ix-asm-lib");
+
+ Assert.NotNull(content);
+ Assert.Contains("[assembly: AXSharp.Connector.SourceLibrary(@\"@ax/sourcefile\", @\"0.0.0\")]", content);
+ }
+
+ [Fact]
+ public void Repository_mode_emits_SourceRepository_assembly_attribute()
+ {
+ var projectFolder = Path.Combine(testFolder, "samples", "sourcefile");
+ var origin = SourceOrigin.Repository(projectFolder, "https://github.com/org/repo", "deadbeef", "main");
+
+ var content = GenerateAssemblyAttributes(new FixedOriginProvider(origin), "ix-asm-repo");
+
+ Assert.NotNull(content);
+ Assert.Contains(
+ "[assembly: AXSharp.Connector.SourceRepository(@\"https://github.com/org/repo\", @\"deadbeef\", @\"main\")]",
+ content);
+ }
+
+ [Fact]
+ public void None_mode_emits_no_assembly_attributes_file()
+ {
+ var content = GenerateAssemblyAttributes(new NoSourceOriginProvider(), "ix-asm-none");
+
+ Assert.Null(content);
+ }
+
+ private string? GenerateAssemblyAttributes(ISourceOriginProvider? provider, string outputSubFolder)
+ {
+ var projectFolder = Path.Combine(testFolder, "samples", "sourcefile");
+ var options = new CompilerTestOptions
+ {
+ TargetPlatfromMoniker = "ax",
+ OutputProjectFolder = Path.Combine("samples", "sourcefile", outputSubFolder)
+ };
+
+ var sourceFile = Path.Combine(projectFolder, "src", "sub", "widget.st");
+ var project = new AXSharpProject(
+ new AxProject(projectFolder, new[] { sourceFile }),
+ new[] { typeof(CsOnlinerSourceBuilder) },
+ typeof(CsProject),
+ options,
+ sourceOriginProvider: provider);
+
+ Policy
+ .Handle()
+ .WaitAndRetry(10, a => TimeSpan.FromSeconds(2))
+ .Execute(() =>
+ {
+ if (Directory.Exists(project.OutputFolder)) Directory.Delete(project.OutputFolder, true);
+ });
+
+ project.Generate();
+
+ var asmPath = Path.Combine(project.OutputFolder, ".g", "AssemblyAttributes.g.cs");
+ return File.Exists(asmPath) ? File.ReadAllText(asmPath) : null;
+ }
+}
diff --git a/src/AXSharp.compiler/tests/AXSharp.Compiler.CsTests/Cs/CompilerTestOptions.cs b/src/AXSharp.compiler/tests/AXSharp.Compiler.CsTests/Cs/CompilerTestOptions.cs
index 077d99f3..aa6f19cd 100644
--- a/src/AXSharp.compiler/tests/AXSharp.Compiler.CsTests/Cs/CompilerTestOptions.cs
+++ b/src/AXSharp.compiler/tests/AXSharp.Compiler.CsTests/Cs/CompilerTestOptions.cs
@@ -29,4 +29,8 @@ public string? OutputProjectFolder
public bool SkipDependencyCompilation { get => false; set { } }
public string TargetPlatfromMoniker { get; set; } = "ax";
public string? UiHostProject { get; set; }
+
+ // Tests default to legacy behavior (no provenance emission) so existing goldens stay deterministic
+ // and repository-independent. Provenance behavior is exercised by injecting explicit providers.
+ public string SourceOrigin { get; set; } = "off";
}
\ No newline at end of file
diff --git a/src/AXSharp.compiler/tests/AXSharp.Compiler.CsTests/SourceFileAttributeRepositoryModeTests.cs b/src/AXSharp.compiler/tests/AXSharp.Compiler.CsTests/SourceFileAttributeRepositoryModeTests.cs
new file mode 100644
index 00000000..9f323e4b
--- /dev/null
+++ b/src/AXSharp.compiler/tests/AXSharp.Compiler.CsTests/SourceFileAttributeRepositoryModeTests.cs
@@ -0,0 +1,88 @@
+// AXSharp.Compiler.CsTests
+// Copyright (c) 2023 MTS spol. s r.o., and Contributors. All Rights Reserved.
+// Contributors: https://github.com/inxton/axsharp/graphs/contributors
+// See the LICENSE file in the repository root for more information.
+// https://github.com/inxton/axsharp/blob/dev/LICENSE
+// Third party licenses: https://github.com/inxton/axsharp/blob/master/notices.md
+
+using System;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using AXSharp.Compiler;
+using AXSharp.Compiler.Cs;
+using AXSharp.Compiler.Cs.Onliner;
+using AXSharp.Compiler.Cs.Plain;
+using Polly;
+using Xunit;
+
+namespace AXSharp.Compiler.CsTests;
+
+public class SourceFileAttributeRepositoryModeTests
+{
+ private readonly string testFolder;
+
+ public SourceFileAttributeRepositoryModeTests()
+ {
+#pragma warning disable CS8604
+ var executingAssemblyFileInfo = new FileInfo(Assembly.GetExecutingAssembly().FullName);
+#pragma warning restore CS8604
+ testFolder = executingAssemblyFileInfo.Directory!.FullName;
+ }
+
+ private sealed class FixedOriginProvider : ISourceOriginProvider
+ {
+ private readonly SourceOrigin _origin;
+ public FixedOriginProvider(SourceOrigin origin) => _origin = origin;
+ public SourceOrigin Resolve(AxProject project) => _origin;
+ }
+
+ // In repository mode the per-type path is relativized against the repository root (here the
+ // sample project folder), so the widget under src/sub becomes "src/sub/widget.st".
+ [Theory]
+ [InlineData(typeof(CsOnlinerSourceBuilder))]
+ [InlineData(typeof(CsPlainSourceBuilder))]
+ public void emits_repository_rooted_source_path(Type builder)
+ {
+ var projectFolder = Path.Combine(testFolder, "samples", "sourcefile");
+ var origin = SourceOrigin.Repository(projectFolder, "https://github.com/org/repo", "deadbeef", "main");
+
+ var content = Generate(builder, new FixedOriginProvider(origin), "ix-repo");
+
+ Assert.Contains("[AXSharp.Connector.SourceFileAttribute(@\"src/sub/widget.st\")]", content);
+ Assert.DoesNotContain("[AXSharp.Connector.SourceFileAttribute(@\"sub/widget.st\")]", content);
+ }
+
+ private string Generate(Type builder, ISourceOriginProvider provider, string outputSubFolder)
+ {
+ var projectFolder = Path.Combine(testFolder, "samples", "sourcefile");
+ var options = new CompilerTestOptions
+ {
+ TargetPlatfromMoniker = "ax",
+ OutputProjectFolder = Path.Combine("samples", "sourcefile", outputSubFolder, builder.Name)
+ };
+
+ var sourceFile = Path.Combine(projectFolder, "src", "sub", "widget.st");
+ var project = new AXSharpProject(
+ new AxProject(projectFolder, new[] { sourceFile }),
+ new[] { builder },
+ typeof(CsProject),
+ options,
+ sourceOriginProvider: provider);
+
+ Policy
+ .Handle()
+ .WaitAndRetry(10, a => TimeSpan.FromSeconds(2))
+ .Execute(() =>
+ {
+ if (Directory.Exists(project.OutputFolder)) Directory.Delete(project.OutputFolder, true);
+ });
+
+ project.Generate();
+
+ var generated = Directory
+ .EnumerateFiles(project.OutputFolder, "widget.g.cs", SearchOption.AllDirectories)
+ .Single();
+ return File.ReadAllText(generated);
+ }
+}
diff --git a/src/AXSharp.compiler/tests/AXSharp.CompilerTests/AXSharp.CompilerTests.csproj b/src/AXSharp.compiler/tests/AXSharp.CompilerTests/AXSharp.CompilerTests.csproj
index 09f5f484..22abd222 100644
--- a/src/AXSharp.compiler/tests/AXSharp.CompilerTests/AXSharp.CompilerTests.csproj
+++ b/src/AXSharp.compiler/tests/AXSharp.CompilerTests/AXSharp.CompilerTests.csproj
@@ -25,6 +25,7 @@
+
diff --git a/src/AXSharp.compiler/tests/AXSharp.CompilerTests/CompilerTestOptions.cs b/src/AXSharp.compiler/tests/AXSharp.CompilerTests/CompilerTestOptions.cs
index f6f5c7f8..79a37430 100644
--- a/src/AXSharp.compiler/tests/AXSharp.CompilerTests/CompilerTestOptions.cs
+++ b/src/AXSharp.compiler/tests/AXSharp.CompilerTests/CompilerTestOptions.cs
@@ -31,4 +31,7 @@ public string? OutputProjectFolder
public bool SkipDependencyCompilation { get => false; set { } }
public string TargetPlatfromMoniker { get; set; } = "ax";
public string? UiHostProject { get; set; }
+
+ // Tests default to legacy behavior (no provenance emission) for deterministic, repo-independent output.
+ public string SourceOrigin { get; set; } = "off";
}
\ No newline at end of file
diff --git a/src/AXSharp.compiler/tests/AXSharp.CompilerTests/GitUrlNormalizerTests.cs b/src/AXSharp.compiler/tests/AXSharp.CompilerTests/GitUrlNormalizerTests.cs
new file mode 100644
index 00000000..65ffe840
--- /dev/null
+++ b/src/AXSharp.compiler/tests/AXSharp.CompilerTests/GitUrlNormalizerTests.cs
@@ -0,0 +1,45 @@
+// AXSharp.CompilerTests
+// Copyright (c) 2023 MTS spol. s r.o., and Contributors. All Rights Reserved.
+// Contributors: https://github.com/inxton/axsharp/graphs/contributors
+// See the LICENSE file in the repository root for more information.
+// https://github.com/inxton/axsharp/blob/dev/LICENSE
+// Third party licenses: https://github.com/inxton/axsharp/blob/master/notices.md
+
+using AXSharp.Compiler;
+
+namespace AXSharp.CompilerTests;
+
+public class GitUrlNormalizerTests
+{
+ [Theory]
+ // scp-like SSH syntax -> https, strip .git
+ [InlineData("git@github.com:org/repo.git", "https://github.com/org/repo")]
+ [InlineData("git@github.com:org/repo", "https://github.com/org/repo")]
+ // ssh:// scheme -> https, drop user + port, strip .git
+ [InlineData("ssh://git@github.com/org/repo.git", "https://github.com/org/repo")]
+ [InlineData("ssh://git@github.com:22/org/repo.git", "https://github.com/org/repo")]
+ // https with embedded credentials -> stripped; .git stripped
+ [InlineData("https://user:token@github.com/org/repo.git", "https://github.com/org/repo")]
+ // already clean -> unchanged
+ [InlineData("https://github.com/org/repo", "https://github.com/org/repo")]
+ // trailing slash trimmed
+ [InlineData("https://github.com/org/repo/", "https://github.com/org/repo")]
+ public void Normalizes_remote_to_canonical_https(string raw, string expected)
+ {
+ Assert.Equal(expected, GitUrlNormalizer.Normalize(raw));
+ }
+
+ [Fact]
+ public void Leaves_unrecognized_input_untouched()
+ {
+ Assert.Equal("some-local-path", GitUrlNormalizer.Normalize("some-local-path"));
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ public void Returns_empty_for_blank(string? raw)
+ {
+ Assert.Equal(string.Empty, GitUrlNormalizer.Normalize(raw));
+ }
+}
diff --git a/src/AXSharp.compiler/tests/AXSharp.CompilerTests/SourceOrigin/ApaxSourceOriginProviderTests.cs b/src/AXSharp.compiler/tests/AXSharp.CompilerTests/SourceOrigin/ApaxSourceOriginProviderTests.cs
new file mode 100644
index 00000000..669337f2
--- /dev/null
+++ b/src/AXSharp.compiler/tests/AXSharp.CompilerTests/SourceOrigin/ApaxSourceOriginProviderTests.cs
@@ -0,0 +1,49 @@
+// AXSharp.CompilerTests
+// Copyright (c) 2023 MTS spol. s r.o., and Contributors. All Rights Reserved.
+// Contributors: https://github.com/inxton/axsharp/graphs/contributors
+// See the LICENSE file in the repository root for more information.
+// https://github.com/inxton/axsharp/blob/dev/LICENSE
+// Third party licenses: https://github.com/inxton/axsharp/blob/master/notices.md
+
+using System.IO;
+using System.Reflection;
+using AXSharp.Compiler;
+
+namespace AXSharp.CompilerTests.SourceOrigin;
+
+public class ApaxSourceOriginProviderTests
+{
+ private readonly string testFolder;
+
+ public ApaxSourceOriginProviderTests()
+ {
+ var fi = new FileInfo(Assembly.GetExecutingAssembly().FullName!);
+ testFolder = fi.Directory!.FullName;
+ }
+
+ [Fact]
+ public void Resolves_library_mode_rooted_at_src()
+ {
+ var project = new AxProject(Path.Combine(testFolder, "samples", "units"));
+
+ var origin = new ApaxSourceOriginProvider().Resolve(project);
+
+ Assert.Equal(SourceOriginMode.Library, origin.Mode);
+ Assert.Equal("units", origin.PackageName);
+ Assert.Equal("0.0.0", origin.PackageVersion);
+ Assert.Equal(project.SrcFolder, origin.BaseFolder);
+ }
+
+ [Fact]
+ public void Maps_missing_apax_metadata_to_empty_strings()
+ {
+ using var fixture = new TempAxProjectFixture("type: app\n");
+ var project = new AxProject(fixture.Root);
+
+ var origin = new ApaxSourceOriginProvider().Resolve(project);
+
+ Assert.Equal(SourceOriginMode.Library, origin.Mode);
+ Assert.Equal(string.Empty, origin.PackageName);
+ Assert.Equal(string.Empty, origin.PackageVersion);
+ }
+}
diff --git a/src/AXSharp.compiler/tests/AXSharp.CompilerTests/SourceOrigin/GitSourceOriginProviderTests.cs b/src/AXSharp.compiler/tests/AXSharp.CompilerTests/SourceOrigin/GitSourceOriginProviderTests.cs
new file mode 100644
index 00000000..44ae8aeb
--- /dev/null
+++ b/src/AXSharp.compiler/tests/AXSharp.CompilerTests/SourceOrigin/GitSourceOriginProviderTests.cs
@@ -0,0 +1,83 @@
+// AXSharp.CompilerTests
+// Copyright (c) 2023 MTS spol. s r.o., and Contributors. All Rights Reserved.
+// Contributors: https://github.com/inxton/axsharp/graphs/contributors
+// See the LICENSE file in the repository root for more information.
+// https://github.com/inxton/axsharp/blob/dev/LICENSE
+// Third party licenses: https://github.com/inxton/axsharp/blob/master/notices.md
+
+using System;
+using AXSharp.Compiler;
+using LibGit2Sharp;
+
+namespace AXSharp.CompilerTests.SourceOrigin;
+
+public class GitSourceOriginProviderTests
+{
+ private const string Apax = "name: gitsample\nversion: 1.0.0\ntype: app\n";
+
+ private static void InitRepo(string dir, string? remoteUrl)
+ {
+ Repository.Init(dir);
+ using var repo = new Repository(dir);
+ Commands.Stage(repo, "*");
+ var sig = new Signature("tester", "tester@example.com", DateTimeOffset.UnixEpoch.AddDays(1));
+ repo.Commit("init", sig, sig);
+ if (remoteUrl != null)
+ repo.Network.Remotes.Add("origin", remoteUrl);
+ }
+
+ [Fact]
+ public void Resolves_repository_mode_with_normalized_url_sha_and_branch()
+ {
+ using var fixture = new TempAxProjectFixture(Apax);
+ InitRepo(fixture.Root, "git@github.com:org/repo.git");
+
+ string expectedSha, expectedBranch;
+ using (var repo = new Repository(fixture.Root))
+ {
+ expectedSha = repo.Head.Tip.Sha;
+ expectedBranch = repo.Head.FriendlyName;
+ }
+
+ var origin = new GitSourceOriginProvider().Resolve(new AxProject(fixture.Root));
+
+ Assert.Equal(SourceOriginMode.Repository, origin.Mode);
+ Assert.Equal("https://github.com/org/repo", origin.Url);
+ Assert.Equal(expectedSha, origin.Commit);
+ Assert.Equal(expectedBranch, origin.Branch);
+ Assert.Equal(
+ TempAxProjectFixture.NormalizeDir(fixture.Root),
+ TempAxProjectFixture.NormalizeDir(origin.BaseFolder));
+ }
+
+ [Fact]
+ public void Falls_back_to_library_when_no_remote()
+ {
+ using var fixture = new TempAxProjectFixture(Apax);
+ InitRepo(fixture.Root, remoteUrl: null);
+
+ var project = new AxProject(fixture.Root);
+ var origin = new GitSourceOriginProvider().Resolve(project);
+
+ Assert.Equal(SourceOriginMode.Library, origin.Mode);
+ Assert.Equal("gitsample", origin.PackageName);
+ Assert.Equal("1.0.0", origin.PackageVersion);
+ Assert.Equal(project.SrcFolder, origin.BaseFolder);
+ }
+
+ [Fact]
+ public void Emits_empty_branch_on_detached_head()
+ {
+ using var fixture = new TempAxProjectFixture(Apax);
+ InitRepo(fixture.Root, "git@github.com:org/repo.git");
+ using (var repo = new Repository(fixture.Root))
+ {
+ Commands.Checkout(repo, repo.Head.Tip); // detach HEAD at the tip commit
+ }
+
+ var origin = new GitSourceOriginProvider().Resolve(new AxProject(fixture.Root));
+
+ Assert.Equal(SourceOriginMode.Repository, origin.Mode);
+ Assert.Equal(string.Empty, origin.Branch);
+ }
+}
diff --git a/src/AXSharp.compiler/tests/AXSharp.CompilerTests/SourceOrigin/NoSourceOriginProviderTests.cs b/src/AXSharp.compiler/tests/AXSharp.CompilerTests/SourceOrigin/NoSourceOriginProviderTests.cs
new file mode 100644
index 00000000..9774519d
--- /dev/null
+++ b/src/AXSharp.compiler/tests/AXSharp.CompilerTests/SourceOrigin/NoSourceOriginProviderTests.cs
@@ -0,0 +1,34 @@
+// AXSharp.CompilerTests
+// Copyright (c) 2023 MTS spol. s r.o., and Contributors. All Rights Reserved.
+// Contributors: https://github.com/inxton/axsharp/graphs/contributors
+// See the LICENSE file in the repository root for more information.
+// https://github.com/inxton/axsharp/blob/dev/LICENSE
+// Third party licenses: https://github.com/inxton/axsharp/blob/master/notices.md
+
+using System.IO;
+using System.Reflection;
+using AXSharp.Compiler;
+
+namespace AXSharp.CompilerTests.SourceOrigin;
+
+public class NoSourceOriginProviderTests
+{
+ private readonly string testFolder;
+
+ public NoSourceOriginProviderTests()
+ {
+ var fi = new FileInfo(Assembly.GetExecutingAssembly().FullName!);
+ testFolder = fi.Directory!.FullName;
+ }
+
+ [Fact]
+ public void Resolves_none_mode_rooted_at_src()
+ {
+ var project = new AxProject(Path.Combine(testFolder, "samples", "units"));
+
+ var origin = new NoSourceOriginProvider().Resolve(project);
+
+ Assert.Equal(SourceOriginMode.None, origin.Mode);
+ Assert.Equal(project.SrcFolder, origin.BaseFolder);
+ }
+}
diff --git a/src/AXSharp.compiler/tests/AXSharp.CompilerTests/SourceOrigin/SourceOriginProviderFactoryTests.cs b/src/AXSharp.compiler/tests/AXSharp.CompilerTests/SourceOrigin/SourceOriginProviderFactoryTests.cs
new file mode 100644
index 00000000..9250b34c
--- /dev/null
+++ b/src/AXSharp.compiler/tests/AXSharp.CompilerTests/SourceOrigin/SourceOriginProviderFactoryTests.cs
@@ -0,0 +1,40 @@
+// AXSharp.CompilerTests
+// Copyright (c) 2023 MTS spol. s r.o., and Contributors. All Rights Reserved.
+// Contributors: https://github.com/inxton/axsharp/graphs/contributors
+// See the LICENSE file in the repository root for more information.
+// https://github.com/inxton/axsharp/blob/dev/LICENSE
+// Third party licenses: https://github.com/inxton/axsharp/blob/master/notices.md
+
+using AXSharp.Compiler;
+
+namespace AXSharp.CompilerTests.SourceOrigin;
+
+public class SourceOriginProviderFactoryTests
+{
+ [Theory]
+ [InlineData("auto")]
+ [InlineData("AUTO")]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData("nonsense")]
+ public void Defaults_to_git_auto_detection(string? mode)
+ {
+ Assert.IsType(SourceOriginProviderFactory.Create(mode));
+ }
+
+ [Theory]
+ [InlineData("apax")]
+ [InlineData("APAX")]
+ public void Apax_forces_library_mode(string mode)
+ {
+ Assert.IsType(SourceOriginProviderFactory.Create(mode));
+ }
+
+ [Theory]
+ [InlineData("off")]
+ [InlineData("Off")]
+ public void Off_disables_emission(string mode)
+ {
+ Assert.IsType(SourceOriginProviderFactory.Create(mode));
+ }
+}
diff --git a/src/AXSharp.compiler/tests/AXSharp.CompilerTests/SourceOrigin/TempAxProjectFixture.cs b/src/AXSharp.compiler/tests/AXSharp.CompilerTests/SourceOrigin/TempAxProjectFixture.cs
new file mode 100644
index 00000000..ca65301b
--- /dev/null
+++ b/src/AXSharp.compiler/tests/AXSharp.CompilerTests/SourceOrigin/TempAxProjectFixture.cs
@@ -0,0 +1,45 @@
+// AXSharp.CompilerTests
+// Copyright (c) 2023 MTS spol. s r.o., and Contributors. All Rights Reserved.
+// Contributors: https://github.com/inxton/axsharp/graphs/contributors
+// See the LICENSE file in the repository root for more information.
+// https://github.com/inxton/axsharp/blob/dev/LICENSE
+// Third party licenses: https://github.com/inxton/axsharp/blob/master/notices.md
+
+using System;
+using System.IO;
+
+namespace AXSharp.CompilerTests.SourceOrigin;
+
+///
+/// Creates a throwaway on-disk AX project (apax.yml + src/ with one .st file) under the temp
+/// folder. Disposing removes the whole tree. Used by source-origin provider tests that need a
+/// real without polluting the repo with fixtures.
+///
+internal sealed class TempAxProjectFixture : IDisposable
+{
+ public TempAxProjectFixture(string apaxYml)
+ {
+ Root = Path.Combine(Path.GetTempPath(), "axsharp-origin-tests", Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(Path.Combine(Root, "src"));
+ File.WriteAllText(Path.Combine(Root, "apax.yml"), apaxYml);
+ File.WriteAllText(Path.Combine(Root, "src", "dummy.st"), "CLASS dummy END_CLASS");
+ }
+
+ public string Root { get; }
+
+ public void Dispose()
+ {
+ try
+ {
+ if (Directory.Exists(Root))
+ Directory.Delete(Root, true);
+ }
+ catch
+ {
+ // best-effort cleanup
+ }
+ }
+
+ public static string NormalizeDir(string path) =>
+ Path.TrimEndingDirectorySeparator(Path.GetFullPath(path));
+}
diff --git a/src/AXSharp.compiler/tests/AXSharp.ixc.Tests/CliProgramTest.cs b/src/AXSharp.compiler/tests/AXSharp.ixc.Tests/CliProgramTest.cs
index e39006c5..27a44f1e 100644
--- a/src/AXSharp.compiler/tests/AXSharp.ixc.Tests/CliProgramTest.cs
+++ b/src/AXSharp.compiler/tests/AXSharp.ixc.Tests/CliProgramTest.cs
@@ -46,7 +46,8 @@ public void should_run_with_default_settings()
ixc.Program.Main(new string[0]);
Assert.True(Directory.Exists(outputDirectory));
- Assert.Equal(7, Directory.EnumerateFiles(outputDirectory, "*.*", SearchOption.AllDirectories).Count());
+ // +1 vs legacy: auto mode emits .g/AssemblyAttributes.g.cs (source provenance).
+ Assert.Equal(8, Directory.EnumerateFiles(outputDirectory, "*.*", SearchOption.AllDirectories).Count());
}
catch
{
@@ -57,7 +58,7 @@ public void should_run_with_default_settings()
Environment.CurrentDirectory = recoverDirectory;
}
}
-
+
[Fact]
public void should_run_with_setting_retrieved_from_config_file_settings()
{
@@ -79,7 +80,8 @@ public void should_run_with_setting_retrieved_from_config_file_settings()
Assert.True(Directory.Exists(outputDirectory));
- Assert.Equal(9, Directory.EnumerateFiles(outputDirectory, "*.*", SearchOption.AllDirectories).Count());
+ // +1 vs legacy: auto mode emits .g/AssemblyAttributes.g.cs (source provenance).
+ Assert.Equal(10, Directory.EnumerateFiles(outputDirectory, "*.*", SearchOption.AllDirectories).Count());
}
catch
{
@@ -108,7 +110,8 @@ public void should_run_with_setting_retrieved_from_config_file_settings_but_over
Assert.True(Directory.Exists(outputDirectory));
- Assert.Equal(7, Directory.EnumerateFiles(outputDirectory, "*.*", SearchOption.AllDirectories).Count());
+ // +1 vs legacy: auto mode emits .g/AssemblyAttributes.g.cs (source provenance).
+ Assert.Equal(8, Directory.EnumerateFiles(outputDirectory, "*.*", SearchOption.AllDirectories).Count());
}
catch
{
diff --git a/src/AXSharp.compiler/tests/integration/actual/app/AXSharp.config.json b/src/AXSharp.compiler/tests/integration/actual/app/AXSharp.config.json
index 199218a4..73b4f638 100644
--- a/src/AXSharp.compiler/tests/integration/actual/app/AXSharp.config.json
+++ b/src/AXSharp.compiler/tests/integration/actual/app/AXSharp.config.json
@@ -1 +1 @@
-{"OutputProjectFolder":"samples\\units\\ix\\tia","UseBase":false,"NoDependencyUpdate":false,"IgnoreS7Pragmas":false,"SkipDependencyCompilation":false,"TargetPlatfromMoniker":"tia","ProjectFile":"app.csproj","UiHostProject":null}
\ No newline at end of file
+{"OutputProjectFolder":"samples\\units\\ix\\tia","UseBase":false,"NoDependencyUpdate":false,"IgnoreS7Pragmas":false,"SkipDependencyCompilation":false,"TargetPlatfromMoniker":"tia","ProjectFile":"app.csproj","UiHostProject":null,"SourceOrigin":"off"}
\ No newline at end of file
diff --git a/src/AXSharp.compiler/tests/integration/actual/lib1/AXSharp.config.json b/src/AXSharp.compiler/tests/integration/actual/lib1/AXSharp.config.json
index 0e93f4af..4769fd0a 100644
--- a/src/AXSharp.compiler/tests/integration/actual/lib1/AXSharp.config.json
+++ b/src/AXSharp.compiler/tests/integration/actual/lib1/AXSharp.config.json
@@ -1 +1 @@
-{"OutputProjectFolder":"samples\\units\\ix\\tia","UseBase":false,"NoDependencyUpdate":false,"IgnoreS7Pragmas":false,"SkipDependencyCompilation":false,"TargetPlatfromMoniker":"tia","ProjectFile":"lib1.csproj","UiHostProject":null}
\ No newline at end of file
+{"OutputProjectFolder":"samples\\units\\ix\\tia","UseBase":false,"NoDependencyUpdate":false,"IgnoreS7Pragmas":false,"SkipDependencyCompilation":false,"TargetPlatfromMoniker":"tia","ProjectFile":"lib1.csproj","UiHostProject":null,"SourceOrigin":"off"}
\ No newline at end of file
diff --git a/src/AXSharp.compiler/tests/integration/actual/lib2/AXSharp.config.json b/src/AXSharp.compiler/tests/integration/actual/lib2/AXSharp.config.json
index 60710e79..fc14fe76 100644
--- a/src/AXSharp.compiler/tests/integration/actual/lib2/AXSharp.config.json
+++ b/src/AXSharp.compiler/tests/integration/actual/lib2/AXSharp.config.json
@@ -1 +1 @@
-{"OutputProjectFolder":"samples\\units\\ix\\tia","UseBase":false,"NoDependencyUpdate":false,"IgnoreS7Pragmas":false,"SkipDependencyCompilation":false,"TargetPlatfromMoniker":"tia","ProjectFile":"lib2.csproj","UiHostProject":null}
\ No newline at end of file
+{"OutputProjectFolder":"samples\\units\\ix\\tia","UseBase":false,"NoDependencyUpdate":false,"IgnoreS7Pragmas":false,"SkipDependencyCompilation":false,"TargetPlatfromMoniker":"tia","ProjectFile":"lib2.csproj","UiHostProject":null,"SourceOrigin":"off"}
\ No newline at end of file
diff --git a/src/AXSharp.connectors/src/AXSharp.Connector/Attributes/SourceFileAttribute.cs b/src/AXSharp.connectors/src/AXSharp.Connector/Attributes/SourceFileAttribute.cs
index d3acbd96..e065e35e 100644
--- a/src/AXSharp.connectors/src/AXSharp.Connector/Attributes/SourceFileAttribute.cs
+++ b/src/AXSharp.connectors/src/AXSharp.Connector/Attributes/SourceFileAttribute.cs
@@ -11,7 +11,10 @@ namespace AXSharp.Connector;
///
/// Indicates the AX/Structured-Text source file from which this type was transpiled.
-/// The path is relative to the project src folder and forward-slash separated.
+/// The path is forward-slash separated; its base depends on the assembly-level provenance
+/// attribute: it is relative to the repository root when
+/// is present on the assembly, otherwise relative to the project src folder (when
+/// is present, or in legacy output with neither).
///
///
/// This attribute is emitted in the connector building process. It should not be declared by the
diff --git a/src/AXSharp.connectors/src/AXSharp.Connector/Attributes/SourceLibraryAttribute.cs b/src/AXSharp.connectors/src/AXSharp.Connector/Attributes/SourceLibraryAttribute.cs
new file mode 100644
index 00000000..5de5a4fa
--- /dev/null
+++ b/src/AXSharp.connectors/src/AXSharp.Connector/Attributes/SourceLibraryAttribute.cs
@@ -0,0 +1,38 @@
+using System;
+
+namespace AXSharp.Connector;
+
+///
+/// Identifies the apax package (name + version) the transpiled types in this assembly originate
+/// from. This attribute is emitted when no usable git remote is available; when it is present,
+/// the per-type path is relative to the project src
+/// folder (forward-slash separated).
+///
+///
+/// This attribute is emitted in the connector building process. It should not be declared by the
+/// framework consumers.
+///
+[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)]
+public class SourceLibraryAttribute : Attribute
+{
+ ///
+ /// Creates a new instance of .
+ ///
+ /// apax package name.
+ /// apax package version.
+ public SourceLibraryAttribute(string name, string version)
+ {
+ Name = name ?? throw new ArgumentNullException(nameof(name));
+ Version = version ?? throw new ArgumentNullException(nameof(version));
+ }
+
+ ///
+ /// Gets the apax package name.
+ ///
+ public string Name { get; }
+
+ ///
+ /// Gets the apax package version.
+ ///
+ public string Version { get; }
+}
diff --git a/src/AXSharp.connectors/src/AXSharp.Connector/Attributes/SourceRepositoryAttribute.cs b/src/AXSharp.connectors/src/AXSharp.Connector/Attributes/SourceRepositoryAttribute.cs
new file mode 100644
index 00000000..5be6af57
--- /dev/null
+++ b/src/AXSharp.connectors/src/AXSharp.Connector/Attributes/SourceRepositoryAttribute.cs
@@ -0,0 +1,45 @@
+using System;
+
+namespace AXSharp.Connector;
+
+///
+/// Identifies the remote git repository the transpiled types in this assembly originate from.
+/// When this attribute is present, the per-type path is
+/// relative to the repository root (forward-slash separated). Combined with
+/// , a source permalink can be formed as {Url}/blob/{Commit}/{SourceFile}.
+///
+///
+/// This attribute is emitted in the connector building process. It should not be declared by the
+/// framework consumers.
+///
+[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)]
+public class SourceRepositoryAttribute : Attribute
+{
+ ///
+ /// Creates a new instance of .
+ ///
+ /// Canonical (https) remote repository URL.
+ /// Full commit SHA the sources were transpiled from.
+ /// Branch name; empty when the repository is in a detached-HEAD state.
+ public SourceRepositoryAttribute(string url, string commit, string branch)
+ {
+ Url = url ?? throw new ArgumentNullException(nameof(url));
+ Commit = commit ?? throw new ArgumentNullException(nameof(commit));
+ Branch = branch ?? throw new ArgumentNullException(nameof(branch));
+ }
+
+ ///
+ /// Gets the canonical (https) remote repository URL.
+ ///
+ public string Url { get; }
+
+ ///
+ /// Gets the full commit SHA the sources were transpiled from.
+ ///
+ public string Commit { get; }
+
+ ///
+ /// Gets the branch name; empty when the repository is in a detached-HEAD state.
+ ///
+ public string Branch { get; }
+}
diff --git a/src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Attributes/SourceLibraryAttributeTests.cs b/src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Attributes/SourceLibraryAttributeTests.cs
new file mode 100644
index 00000000..42ccbbad
--- /dev/null
+++ b/src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Attributes/SourceLibraryAttributeTests.cs
@@ -0,0 +1,37 @@
+// AXSharp.ConnectorTests
+// Copyright (c) 2023 MTS spol. s r.o., and Contributors. All Rights Reserved.
+// Contributors: https://github.com/inxton/axsharp/graphs/contributors
+// See the LICENSE file in the repository root for more information.
+// https://github.com/inxton/axsharp/blob/dev/LICENSE
+// Third party licenses: https://github.com/inxton/axsharp/blob/master/notices.md
+
+namespace AXSharp.ConnectorTests.Attributes
+{
+ using AXSharp.Connector;
+ using System;
+ using System.Reflection;
+ using Xunit;
+
+ public class SourceLibraryAttributeTests
+ {
+ [Fact]
+ public void Exposes_name_and_version()
+ {
+ var instance = new SourceLibraryAttribute("@ax/foo", "1.2.3");
+
+ Assert.Equal("@ax/foo", instance.Name);
+ Assert.Equal("1.2.3", instance.Version);
+ }
+
+ [Fact]
+ public void Is_assembly_scoped_single_use()
+ {
+ var usage = typeof(SourceLibraryAttribute)
+ .GetCustomAttribute();
+
+ Assert.NotNull(usage);
+ Assert.Equal(AttributeTargets.Assembly, usage!.ValidOn);
+ Assert.False(usage.AllowMultiple);
+ }
+ }
+}
diff --git a/src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Attributes/SourceRepositoryAttributeTests.cs b/src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Attributes/SourceRepositoryAttributeTests.cs
new file mode 100644
index 00000000..23f39bc5
--- /dev/null
+++ b/src/AXSharp.connectors/tests/AXSharp.ConnectorTests/AXSharp.ConnectorTests/Attributes/SourceRepositoryAttributeTests.cs
@@ -0,0 +1,39 @@
+// AXSharp.ConnectorTests
+// Copyright (c) 2023 MTS spol. s r.o., and Contributors. All Rights Reserved.
+// Contributors: https://github.com/inxton/axsharp/graphs/contributors
+// See the LICENSE file in the repository root for more information.
+// https://github.com/inxton/axsharp/blob/dev/LICENSE
+// Third party licenses: https://github.com/inxton/axsharp/blob/master/notices.md
+
+namespace AXSharp.ConnectorTests.Attributes
+{
+ using AXSharp.Connector;
+ using System;
+ using System.Reflection;
+ using Xunit;
+
+ public class SourceRepositoryAttributeTests
+ {
+ [Fact]
+ public void Exposes_url_commit_and_branch()
+ {
+ var instance = new SourceRepositoryAttribute(
+ "https://github.com/org/repo", "a1b2c3d", "main");
+
+ Assert.Equal("https://github.com/org/repo", instance.Url);
+ Assert.Equal("a1b2c3d", instance.Commit);
+ Assert.Equal("main", instance.Branch);
+ }
+
+ [Fact]
+ public void Is_assembly_scoped_single_use()
+ {
+ var usage = typeof(SourceRepositoryAttribute)
+ .GetCustomAttribute();
+
+ Assert.NotNull(usage);
+ Assert.Equal(AttributeTargets.Assembly, usage!.ValidOn);
+ Assert.False(usage.AllowMultiple);
+ }
+ }
+}