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
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,12 @@ public interface ICompilerOptions
/// </summary>
string? UiHostProject { get; set; }

/// <summary>
/// Source-origin detection mode controlling the provenance attributes emitted onto generated
/// twins: <c>auto</c> (detect a git repository with a remote, otherwise fall back to apax
/// package identity), <c>apax</c> (force apax package mode, deterministic, no commit SHA), or
/// <c>off</c> (legacy: src-relative paths and no assembly-level attribute).
/// </summary>
string SourceOrigin { get; set; }

}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="CliWrap" />
<PackageReference Include="CliWrap" />
<PackageReference Include="LibGit2Sharp" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="NuGet.Configuration" />
Expand Down
15 changes: 15 additions & 0 deletions src/AXSharp.compiler/src/AXSharp.Compiler/AXSharpConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ public string OutputProjectFolder
/// </summary>
public string? UiHostProject { get; set; }

private string _sourceOrigin = "auto";

/// <summary>
/// Source-origin detection mode controlling provenance attributes emitted onto generated twins:
/// <c>auto</c> (default), <c>apax</c> or <c>off</c>.
/// </summary>
public string SourceOrigin
{
get => string.IsNullOrWhiteSpace(_sourceOrigin) ? "auto" : _sourceOrigin;
set => _sourceOrigin = value;
}


private string _axProjectFolder;

Expand Down Expand Up @@ -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;
}
}
40 changes: 39 additions & 1 deletion src/AXSharp.compiler/src/AXSharp.Compiler/AXSharpProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@ public class AXSharpProject : IAXSharpProject
/// <param name="cliCompilerOptions">
/// Compiler options from CLI.
/// </param>
public AXSharpProject(AxProject axProject, IEnumerable<Type> builderTypes, Type targetProjectType, ICompilerOptions? cliCompilerOptions = null, ICompilerOptions? dependnantCompilerOptions = null)
public AXSharpProject(AxProject axProject, IEnumerable<Type> 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))
Expand Down Expand Up @@ -76,6 +77,16 @@ public AXSharpProject(AxProject axProject, IEnumerable<Type> builderTypes, Type
/// </summary>
public AxProject AxProject { get; }

private readonly ISourceOriginProvider _sourceOriginProvider;
private SourceOrigin? _sourceOrigin;

/// <summary>
/// Gets the resolved <see cref="SourceOrigin" /> (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.
/// </summary>
public SourceOrigin SourceOrigin => _sourceOrigin ??= _sourceOriginProvider.Resolve(AxProject);

private IEnumerable<Type> BuilderTypes { get; }

/// <summary>
Expand Down Expand Up @@ -201,13 +212,40 @@ public void Generate()
}
}

EmitAssemblyAttributes();

//TargetProject.ProvisionProjectStructure();
GenerateMetadata(compilationResult.Compilation, projectSources);
TargetProject.GenerateResources();
TargetProject.GenerateCompanionData();
Log.Logger.Information($"Compilation of project '{AxProject.SrcFolder}' done.");
}

/// <summary>
/// Emits the single assembly-level provenance attribute (<c>.g/AssemblyAttributes.g.cs</c>)
/// describing the source origin of this project. No file is written in None mode.
/// </summary>
private void EmitAssemblyAttributes()
{
var assemblyAttribute = SourceOriginEmitter.RenderAssemblyAttribute(SourceOrigin);
if (assemblyAttribute == null)
return;

Policy
.Handle<IOException>()
.WaitAndRetry(5, a => TimeSpan.FromMilliseconds(500))
.Execute(() =>
{
using (var swr = new StreamWriter(Path.Combine(
EnsureFolder(Path.Combine(OutputFolder, ".g")),
"AssemblyAttributes.g.cs")))
{
swr.WriteLine("// <auto-generated/>");
swr.WriteLine(assemblyAttribute);
}
});
}



/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Resolves Library-mode provenance from the project's apax metadata. Per-type paths are rooted
/// at the project <c>src</c> folder. Missing name/version map to empty strings.
/// </summary>
public sealed class ApaxSourceOriginProvider : ISourceOriginProvider
{
/// <inheritdoc />
public SourceOrigin Resolve(AxProject project)
=> SourceOrigin.Library(
project.SrcFolder,
project.ProjectInfo.Name ?? string.Empty,
project.ProjectInfo.Version ?? string.Empty);
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Resolves Repository-mode provenance from the nearest enclosing git repository (innermost
/// <c>.git</c> — submodules/worktrees resolve to their own repository) that exposes an
/// <c>origin</c> 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 <c>origin</c> remote is
/// found, resolution delegates to the fallback (apax) provider.
/// </summary>
public sealed class GitSourceOriginProvider : ISourceOriginProvider
{
private readonly ISourceOriginProvider _fallback;

/// <summary>
/// Creates a new instance. <paramref name="fallback" /> is used when the project is not in a
/// git repository with a usable <c>origin</c> remote (defaults to <see cref="ApaxSourceOriginProvider" />).
/// </summary>
public GitSourceOriginProvider(ISourceOriginProvider? fallback = null)
=> _fallback = fallback ?? new ApaxSourceOriginProvider();

/// <inheritdoc />
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);
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Normalizes a git remote URL into a canonical, credential-free <c>https</c> form suitable for
/// embedding in <c>AXSharp.Connector.SourceRepositoryAttribute</c> and for building source
/// permalinks. Best-effort: input that is not recognizable as a remote URL is returned unchanged.
/// </summary>
public static class GitUrlNormalizer
{
// scp-like syntax: [user@]host:org/repo[.git] (no URI scheme)
private static readonly Regex ScpLike = new(@"^(?:[^@/]+@)?([^/:]+):(.+)$", RegexOptions.Compiled);

/// <summary>
/// Normalizes <paramref name="raw" /> to canonical <c>https</c>, stripping any embedded
/// credentials, port and trailing <c>.git</c>/slash. Returns <see cref="string.Empty" /> for
/// null/blank input and the trimmed input verbatim when it is not a recognizable remote URL.
/// </summary>
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}";
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Resolves the <see cref="SourceOrigin" /> (repository or apax-package provenance) for an
/// AX project. Implementations are the seam that lets tests force deterministic provenance.
/// </summary>
public interface ISourceOriginProvider
{
/// <summary>Resolves the provenance for <paramref name="project" />.</summary>
SourceOrigin Resolve(AxProject project);
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Resolves None-mode provenance: no assembly-level attribute is emitted and per-type paths stay
/// rooted at the project <c>src</c> folder. This reproduces the pre-feature (legacy) behavior.
/// </summary>
public sealed class NoSourceOriginProvider : ISourceOriginProvider
{
/// <inheritdoc />
public SourceOrigin Resolve(AxProject project) => SourceOrigin.None(project.SrcFolder);
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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.
/// </summary>
public enum SourceOriginMode
{
/// <summary>Git repository with a usable remote; per-type path is repository-root relative.</summary>
Repository,

/// <summary>apax package; per-type path is <c>src</c>-folder relative.</summary>
Library,

/// <summary>No provenance emitted; per-type path is <c>src</c>-folder relative (legacy).</summary>
None
}

/// <summary>
/// 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.
/// </summary>
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;
}

/// <summary>Gets the resolved provenance mode.</summary>
public SourceOriginMode Mode { get; }

/// <summary>Gets the folder against which per-type source paths are relativized.</summary>
public string BaseFolder { get; }

/// <summary>Gets the canonical remote URL (Repository mode); otherwise <c>null</c>.</summary>
public string? Url { get; }

/// <summary>Gets the commit SHA (Repository mode); otherwise <c>null</c>.</summary>
public string? Commit { get; }

/// <summary>Gets the branch name, empty on detached HEAD (Repository mode); otherwise <c>null</c>.</summary>
public string? Branch { get; }

/// <summary>Gets the apax package name (Library mode); otherwise <c>null</c>.</summary>
public string? PackageName { get; }

/// <summary>Gets the apax package version (Library mode); otherwise <c>null</c>.</summary>
public string? PackageVersion { get; }

/// <summary>Creates a Repository-mode origin rooted at the repository working directory.</summary>
public static SourceOrigin Repository(string baseFolder, string url, string commit, string branch)
=> new(SourceOriginMode.Repository, baseFolder, url, commit, branch, null, null);

/// <summary>Creates a Library-mode origin rooted at the project <c>src</c> folder.</summary>
public static SourceOrigin Library(string baseFolder, string name, string version)
=> new(SourceOriginMode.Library, baseFolder, null, null, null, name, version);

/// <summary>Creates a None-mode origin rooted at the project <c>src</c> folder (legacy, no emission).</summary>
public static SourceOrigin None(string baseFolder)
=> new(SourceOriginMode.None, baseFolder, null, null, null, null, null);
}
Loading
Loading