2 Commits

Author SHA1 Message Date
46c1a78e3f Dotnet 7, publish scripts 2022-11-02 12:03:54 +01:00
fb381e56c5 DevContainer 2022-11-02 12:02:24 +01:00
51 changed files with 211 additions and 1379 deletions

View File

@@ -1,3 +0,0 @@
{
"image": "mcr.microsoft.com/dotnet/sdk:8.0-alpine"
}

View File

@@ -1,3 +0,0 @@
{
"image": "mcr.microsoft.com/dotnet/sdk:8.0"
}

View File

@@ -0,0 +1,3 @@
{
"image": "mcr.microsoft.com/dotnet/sdk:7.0-alpine"
}

View File

@@ -1,2 +0,0 @@
[*.cs]

View File

@@ -1,87 +0,0 @@
name: Alma build
on:
push:
tags:
- "v*"
jobs:
build:
runs-on: ${{ matrix.target.runner }}
strategy:
matrix:
target:
- runtime: win-x64
runner: windows-latest
name: windows
outputname: Alma.exe
version_script: .scripts/versioning.ps1
- runtime: linux-x64
runner: ubuntu-latest
name: linux
outputname: Alma
version_script: .scripts/versioning.sh
- runtime: linux-musl-x64
runner: ubuntu-latest
name: linux-musl
outputname: Alma
version_script: .scripts/versioning.sh
steps:
- uses: actions/checkout@v3
- name: Setup dotnet
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
- name: Patch version
run: ${{ matrix.target.version_script }}
continue-on-error: true
- name: Restore dependencies
run: dotnet restore src/Alma
- name: Build
run: dotnet publish -c Release -p:PublishAot=true -r ${{ matrix.target.runtime }} -o app/ src/Alma
- uses: actions/upload-artifact@v3
with:
name: alma-${{ matrix.target.name }}
path: app/${{ matrix.target.outputname }}
release:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v3
name: Download Windows artifacts
with:
name: alma-windows
path: app/windows/
- uses: actions/download-artifact@v3
name: Download Linux artifacts
with:
name: alma-linux
path: app/linux/
- uses: actions/download-artifact@v3
name: Download Linux musl artifacts
with:
name: alma-linux
path: app/linux-musl/
- name: Create release directory
run: mkdir release
- name: Copy windows executable
run: cp app/windows/Alma.exe release/alma.exe
- name: Copy linux executable
run: cp app/linux/Alma release/alma-linux
- name: Copy linux-musl executable
run: cp app/linux-musl/Alma release/alma-linux-musl
- uses: "marvinpinto/action-automatic-releases@latest"
name: Create release
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
automatic_release_tag: latest
prerelease: false
draft: true
files: |
release/alma*

View File

@@ -1,12 +0,0 @@
if((Get-Content env:\GITHUB_REF_TYPE) -ne "tag")
{
Return
}
$version = (Get-Content env:\GITHUB_REF).Replace("refs/tags/v", "")
$git_sha = (Get-Content env:\GITHUB_SHA).Substring(0, 8)
Write-Host $version
Write-Host $git_sha
(Get-Content src\Alma\Alma.csproj).Replace("0.0.0", $version).Replace("development", $git_sha) | Set-Content src\Alma\Alma.csproj

View File

@@ -1,13 +0,0 @@
if [ ${GITHUB_REF_TYPE} != "tag" ]; then
exit 1
fi
version="${GITHUB_REF:11}"
git_hash="${GITHUB_SHA}"
echo $git_hash
git_hash=`expr substr $git_hash 1 8`
echo $version
echo $git_hash
sed -i "s/0.0.0/$version/g;s/development/$git_hash/g" src/Alma/Alma.csproj

4
.vscode/launch.json vendored
View File

@@ -10,8 +10,8 @@
"request": "launch", "request": "launch",
"preLaunchTask": "build", "preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path. // If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/src/Alma/bin/Debug/net8.0/Alma.dll", "program": "${workspaceFolder}/src/Alma/bin/Debug/net7.0/Alma.dll",
"args": ["link", "dotconfig", "git", "-d"], "args": [],
"cwd": "${workspaceFolder}/src/Alma", "cwd": "${workspaceFolder}/src/Alma",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole", "console": "internalConsole",

View File

@@ -1,25 +0,0 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine as BUILD
RUN apk add -U clang zlib-dev
WORKDIR /build
COPY . .
RUN dotnet publish src/Alma/Alma.csproj -c Release -r linux-musl-x64 -p:PublishAot=true -o /app
FROM alpine:edge as FINAL
RUN mkdir /data
WORKDIR /data
ENV ALMA_APP_DATA_FALLBACK=/appdata
ENV ALMA_CONFIG_FALLBACK=/appconfig
RUN mkdir /appdata
RUN mkdir /appconfig
RUN apk add -U icu-libs libstdc++
COPY --from=BUILD /app/Alma /alma
ENTRYPOINT ["/alma"]
CMD ["help"]

View File

@@ -1,32 +0,0 @@
# Alma
Alma (aka Advanced Link Manager Application) is another dotfiles management tool.
## Installation
**Linux**
```
sudo wget https://github.com/ADIX7/Alma/releases/download/latest/alma-linux -O /usr/local/bin/alma
sudo chmod +x /usr/local/bin/alma
```
**Windows**
This PowerShell command will download the alma.exe to the current folder. Move it to a folder that is in PATH.
```
Invoke-WebRequest https://github.com/ADIX7/Alma/releases/download/latest/Alma.exe -OutFile alma.exe
```
**As a Docker tool**
You can run it with Docker/Podman. You should mount every directory (source and target too) to the same path as they are on the host.
For example, if you have your dotfiles cloned to your home folder and you have your repository.json in ~/.config/alma/repository.json, then you can run this command and the links will be correct on the host.
```
docker run --rm -it -v /home/myuser:/home/myuser -e "HOME=/home/myuser" adix7/alma:latest ...
```
If you don't have repository.json, you can set the WORKDIR (or ALMA_WORKDIR) env var to your repository. For example, if you cloned your dotfiles to ~/dotfiles, you can use this command.
```
docker run --rm -it -v /home/myuser:/home/myuser -e "WORKDIR=/home/myuser/dotfiles" adix7/alma:latest ...
```

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<RootNamespace>Alma</RootNamespace> <RootNamespace>Alma</RootNamespace>

View File

@@ -2,7 +2,6 @@ namespace Alma.Command;
public interface ICommand public interface ICommand
{ {
string CommandString { get; } public string CommandString { get; }
string[] CommandAliases { get; } public Task Run(List<string> parameters);
Task Run(List<string> parameters);
} }

View File

@@ -4,18 +4,11 @@ public class ModuleConfiguration
{ {
public string? Target { get; set; } public string? Target { get; set; }
public Dictionary<string, string>? Links { get; set; } public Dictionary<string, string>? Links { get; set; }
public List<string>? Exclude { get; set; }
public bool ExcludeReadme { get; set; } = true;
public string? Install { get; set; } public ModuleConfiguration(string? target, Dictionary<string, string>? links)
public string? Configure { get; set; }
public ModuleConfiguration(string? target, Dictionary<string, string>? links, string? install, string? configure)
{ {
Target = target; Target = target;
Links = links; Links = links;
Install = install;
Configure = configure;
} }
public ModuleConfiguration Merge(ModuleConfiguration merge) public ModuleConfiguration Merge(ModuleConfiguration merge)
@@ -24,12 +17,10 @@ public class ModuleConfiguration
.Concat(merge.Links ?? new Dictionary<string, string>()); .Concat(merge.Links ?? new Dictionary<string, string>());
return new ModuleConfiguration( return new ModuleConfiguration(
merge.Target ?? Target, merge.Target ?? Target,
new Dictionary<string, string>(mergedLinks), new Dictionary<string, string>(mergedLinks)
merge.Install ?? Install,
merge.Configure ?? Configure
); );
} }
public static ModuleConfiguration Empty() => public static ModuleConfiguration Empty() =>
new(null, new Dictionary<string, string>(), null, null); new(null, new Dictionary<string, string>());
} }

View File

@@ -1,8 +0,0 @@
namespace Alma.Data;
public static class ColorCodes
{
public const string Reset = "\u001b[0m";
public const string RedForeground = "\u001b[38;5;1m";
public const string GreenForeground = "\u001b[38;5;2m";
}

View File

@@ -1,5 +0,0 @@
using Alma.Configuration.Module;
namespace Alma.Data;
public record ModuleConfigurationWithName(string Name, ModuleConfiguration Configuration);

View File

@@ -1,3 +0,0 @@
namespace Alma.Data;
public record SpecialPathResolver(string PathName, Func<string> Resolver, bool? SkipCombiningCurrentDirectory = null);

View File

@@ -2,6 +2,6 @@ namespace Alma.Services;
public interface IOsInformation public interface IOsInformation
{ {
Task<string> GetOsIdentifierAsync(); string GetOsIdentifier();
Task<bool> IsOnPlatformAsync(string platform); bool IsOnPlatform(string platform);
} }

View File

@@ -1,6 +0,0 @@
namespace Alma.Services;
public interface IPathHelperService
{
string ResolvePath(string path, string? currentDirectory = null);
}

View File

@@ -1,6 +0,0 @@
namespace Alma.Services;
public interface IShellService
{
Task RunCommandAsync(string command);
}

View File

@@ -1,6 +0,0 @@
namespace Alma.Services;
public interface IVersionService
{
public string GetVersion();
}

View File

@@ -5,13 +5,8 @@
<ProjectReference Include="..\Alma.Logging\Alma.Logging.csproj" /> <ProjectReference Include="..\Alma.Logging\Alma.Logging.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="LiteDB" Version="5.0.19" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
</ItemGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<RootNamespace>Alma</RootNamespace> <RootNamespace>Alma</RootNamespace>

View File

@@ -1,4 +1,5 @@
using Alma.Command; using Alma.Command;
using Alma.Command.Help;
using Alma.Logging; using Alma.Logging;
namespace Alma; namespace Alma;
@@ -8,9 +9,9 @@ public class Application
private readonly IList<ICommand> _commands; private readonly IList<ICommand> _commands;
private readonly ILogger<Application> _logger; private readonly ILogger<Application> _logger;
public Application(IEnumerable<ICommand> commands, ILogger<Application> logger) public Application(IEnumerable<ICommand> commands, ILogger<Application> logger, ILogger<HelpCommand> helpCommandLogger)
{ {
_commands = commands.ToList(); _commands = commands.Append(new HelpCommand(() => _commands!, helpCommandLogger)).ToList();
_logger = logger; _logger = logger;
} }
@@ -24,7 +25,7 @@ public class Application
var commandString = args[0]; var commandString = args[0];
var command = _commands.FirstOrDefault(c => c.CommandString == commandString || c.CommandAliases.Contains(commandString)); var command = _commands.FirstOrDefault(c => c.CommandString == commandString);
if (command is null) if (command is null)
{ {

View File

@@ -1,63 +0,0 @@
using Alma.Command.Install;
using Alma.Configuration.Repository;
using Alma.Logging;
using Alma.Services;
namespace Alma.Command.Configure;
public class ConfigureCommand : RepositoryModuleCommandBase
{
private readonly ILogger<InstallCommand> _logger;
private readonly IShellService _shellService;
public override string CommandString => "configure";
public override string[] CommandAliases => Array.Empty<string>();
public ConfigureCommand(
ILogger<InstallCommand> logger,
IRepositoryConfiguration repositoryConfiguration,
IModuleConfigurationResolver moduleConfigurationResolver,
IShellService shellService,
IPathHelperService pathHelperService)
: base(repositoryConfiguration, pathHelperService, moduleConfigurationResolver)
{
_logger = logger;
_shellService = shellService;
}
public override async Task Run(List<string> parameters)
{
var (repoName, moduleName) = GetRepositoryAndModuleName(parameters);
if (moduleName is null)
{
_logger.LogInformation("No module specified");
return;
}
var (moduleConfiguration, _) = await GetModuleConfiguration(repoName, moduleName);
if (moduleConfiguration is null)
{
_logger.LogInformation($"No module configuration found for module '{moduleName}'{(repoName is null ? "" : $" in repository '{repoName}'")}");
return;
}
var configureLines = moduleConfiguration.Configure?.Split(Environment.NewLine);
if (configureLines is null)
{
_logger.LogInformation("No configure command is found");
return;
}
_logger.LogInformation($"Configure command: {string.Join("\n", configureLines)}");
if (configureLines.Length == 1)
{
_logger.LogInformation("Running configure command '" + configureLines[0] + "'");
await _shellService.RunCommandAsync(configureLines[0]);
}
else
{
_logger.LogError("Multi line scripts are not currently supported");
}
}
}

View File

@@ -1,131 +0,0 @@
using System.Reflection;
using Alma.Logging;
namespace Alma.Command.Diag;
public class DiagCommand : ICommand
{
private readonly ILogger<DiagCommand> _logger;
public string CommandString => "diag";
public string[] CommandAliases => Array.Empty<string>();
private readonly Lazy<IReadOnlyList<MethodInfo>> _diagnosticHelpersLazy;
public DiagCommand(ILogger<DiagCommand> logger)
{
_logger = logger;
_diagnosticHelpersLazy = new Lazy<IReadOnlyList<MethodInfo>>(FindDiagnosticHelpers);
}
public Task Run(List<string> parameters)
{
if (parameters.Count < 1)
{
_logger.LogInformation("No diagnostic helper specified.");
return Task.CompletedTask;
}
var command = parameters[0];
if(command == "--help")
{
_logger.LogInformation(
"""
Usage:
diag list
diag <diag-helper>
"""
);
return Task.CompletedTask;
}
if (command == "list")
{
ListDiagnosticHelpers();
return Task.CompletedTask;
}
var diagnosticHelpers = _diagnosticHelpersLazy.Value;
var helper = diagnosticHelpers.FirstOrDefault(
h => GetDiagnosticHelper(h) is { } attr && attr.Command == command
);
if (helper is null)
{
_logger.LogInformation($"Diagnostic helper {command} is not found.");
return Task.CompletedTask;
}
if (!helper.IsStatic)
{
_logger.LogInformation($"Diagnostic helper {helper.Name} is not static.");
return Task.CompletedTask;
}
HandleHelper(helper, parameters.Skip(1));
return Task.CompletedTask;
}
private void ListDiagnosticHelpers()
{
var diagnosticHelpers = _diagnosticHelpersLazy.Value;
var commands = diagnosticHelpers
.Select(h => GetDiagnosticHelper(h)?.Command)
.OfType<string>()
.Order()
.ToList();
_logger.LogInformation("Available diagnostic helpers:");
foreach (var command in commands)
{
_logger.LogInformation(command);
}
}
private void HandleHelper(MethodInfo helper, IEnumerable<string> parameters)
{
var helperParameters = helper.GetParameters();
var helperArguments = new object[helperParameters.Length];
for (var i = 0; i < helperParameters.Length; i++)
{
var parameterType = helperParameters[i].ParameterType;
if (parameterType == typeof(IEnumerable<string>))
{
helperArguments[i] = parameters;
}
else if (parameterType == typeof(ILogger))
{
helperArguments[i] = _logger;
}
else
{
_logger.LogInformation($"Diagnostic helper {helper.Name} has wrong parameter type, could not resolve '{parameterType}'.");
return;
}
}
helper.Invoke(null, helperArguments);
}
private IReadOnlyList<MethodInfo> FindDiagnosticHelpers()
{
var assemblies = new[]
{
Assembly.GetExecutingAssembly(),
};
return assemblies.SelectMany(
a =>
a
.GetTypes()
.SelectMany(t => t.GetMethods())
.Where(t => t.GetCustomAttributes(typeof(DiagnosticHelper)).Any()))
.ToList()
.AsReadOnly();
}
private DiagnosticHelper? GetDiagnosticHelper(MethodInfo o)
=> o.GetCustomAttributes(typeof(DiagnosticHelper)).FirstOrDefault() as DiagnosticHelper;
}

View File

@@ -1,12 +0,0 @@
namespace Alma.Command.Diag;
[AttributeUsage(AttributeTargets.Method)]
public class DiagnosticHelper : Attribute
{
public string Command { get; }
public DiagnosticHelper(string command)
{
Command = command;
}
}

View File

@@ -9,14 +9,12 @@ public class HelpCommand : ICommand
public string CommandString => "help"; public string CommandString => "help";
public string[] CommandAliases { get; } = ["--help", "-h"];
public HelpCommand( public HelpCommand(
IServiceProvider serviceProvider, Func<IEnumerable<ICommand>> commandsProvider,
ILogger<HelpCommand> logger ILogger<HelpCommand> logger
) )
{ {
_commandsProvider = () => (IEnumerable<ICommand>?)serviceProvider.GetService(typeof(IEnumerable<ICommand>)) ?? throw new ApplicationException(); _commandsProvider = commandsProvider;
_logger = logger; _logger = logger;
} }

View File

@@ -1,216 +0,0 @@
using Alma.Configuration.Module;
using Alma.Configuration.Repository;
using Alma.Data;
using Alma.Logging;
using Alma.Services;
namespace Alma.Command.Info;
public class InfoCommand : RepositoryModuleCommandBase
{
public override string CommandString => "info";
public override string[] CommandAliases => Array.Empty<string>();
private readonly IFolderService _folderService;
private readonly IRepositoryConfiguration _repositoryConfiguration;
private readonly IModuleConfigurationResolver _moduleConfigurationResolver;
private readonly ILogger<InfoCommand> _logger;
private readonly IOsInformation _osInformation;
private readonly IVersionService _versionService;
private readonly IPathHelperService _pathHelperService;
private static readonly List<Func<ModuleConfiguration, string?>> _moduleInfoDetailResolvers = new()
{
(m) =>
{
var linkCount = m.Links?.Count ?? 0;
return linkCount.ToString().PadLeft(3) + $" link{(linkCount > 1 ? "s" : "")}".PadRight(6);
},
(m) => m.Install is not null ? "[Install]" : null,
(m) => m.Configure is not null ? "[Configure]" : null,
};
public InfoCommand(
IFolderService folderService,
IRepositoryConfiguration repositoryConfiguration,
IModuleConfigurationResolver moduleConfigurationResolver,
ILogger<InfoCommand> logger,
IOsInformation osInformation,
IVersionService versionService,
IPathHelperService pathHelperService
) : base(repositoryConfiguration, pathHelperService, moduleConfigurationResolver)
{
_folderService = folderService;
_repositoryConfiguration = repositoryConfiguration;
_moduleConfigurationResolver = moduleConfigurationResolver;
_logger = logger;
_osInformation = osInformation;
_versionService = versionService;
_pathHelperService = pathHelperService;
}
public override async Task Run(List<string> parameters)
{
var (repoName, moduleName) = GetRepositoryAndModuleName(parameters, true);
if (repoName is not null && moduleName is null)
{
await ProcessRepoInfoAsync(repoName);
}
else if (repoName is not null && moduleName is not null)
{
await ProcessModuleInfoAsync(repoName, moduleName);
}
else
{
await ProcessGeneralInfoAsync();
}
}
private async Task ProcessGeneralInfoAsync()
{
_logger.LogInformation("Alma " + _versionService.GetVersion());
_logger.LogInformation("");
_logger.LogInformation("AppData folder: " + _folderService.AppData);
if (_folderService.ConfigRoot is { } configRoot)
{
_logger.LogInformation("Configuration folder: " + configRoot);
}
else
{
_logger.LogInformation("Configuration folder not exists.");
_logger.LogInformation("Preferred configuration folder is: " + Path.Combine(_folderService.GetPreferredConfigurationFolder(), _folderService.ApplicationSubfolderName));
}
_logger.LogInformation("");
_logger.LogInformation($"Platform is '{await _osInformation.GetOsIdentifierAsync()}'");
_logger.LogInformation("");
if (_repositoryConfiguration.Configuration.Repositories is { Count: > 0 } repositories)
{
_logger.LogInformation("Repositories:");
foreach (var repository in repositories)
{
Console.Write(repository.Name);
if (repository.RepositoryPath is not null && !Directory.Exists(_pathHelperService.ResolvePath(repository.RepositoryPath)))
{
Console.Write($" (containing folder not exists {repository.RepositoryPath})");
}
_logger.LogInformation("");
}
}
else
{
_logger.LogInformation("No repositories found");
}
}
private async Task ProcessRepoInfoAsync(string repoName)
{
var (repoSourceDirectory, _) = GetRepositorySourceAndTargetDirectory(repoName);
var repoRoot = new DirectoryInfo(repoSourceDirectory);
var modules = (await TraverseRepoFolder(repoRoot, repoRoot)).OrderBy(e => e.Name).ToList();
var maxNameLength = modules.Max(m => m.Name.Length);
_logger.LogInformation($"Repository '{repoName}' contains {modules.Count} modules:");
_logger.LogInformation("");
foreach (var module in modules)
{
var moduleDetails = _moduleInfoDetailResolvers
.Select(m => m(module.Configuration))
.Where(m => m is not null)
.ToList();
_logger.LogInformation($"{module.Name.PadRight(maxNameLength + 3)} {string.Join(" ", moduleDetails)}");
}
}
async Task<IEnumerable<ModuleConfigurationWithName>> TraverseRepoFolder(DirectoryInfo repoRoot, DirectoryInfo currentDirectory)
{
var modulesFound = Enumerable.Empty<ModuleConfigurationWithName>();
var moduleConfigFileStub = Path.Combine(currentDirectory.FullName, Constants.ModuleConfigFileStub);
var (moduleConfig, _) = await _moduleConfigurationResolver.ResolveModuleConfiguration(moduleConfigFileStub);
if (moduleConfig is not null)
{
var moduleName = currentDirectory.FullName[(repoRoot.FullName.TrimEnd(Path.DirectorySeparatorChar).Length + 1)..].Replace(Path.DirectorySeparatorChar, '/');
modulesFound = modulesFound.Append(new(moduleName, moduleConfig));
}
foreach (var subDir in currentDirectory.GetDirectories())
{
modulesFound = modulesFound.Concat(await TraverseRepoFolder(repoRoot, subDir));
}
return modulesFound;
}
async Task ProcessModuleInfoAsync(string repoName, string moduleName)
{
var (moduleConfiguration, moduleConfigFileName) = await GetModuleConfiguration(repoName, moduleName);
if (moduleConfiguration is null)
{
_logger.LogInformation($"No configuration is found for module '{moduleName}' in repository '{repoName}':");
return;
}
_logger.LogInformation($"Information about module '{moduleName}' in repository '{repoName}':");
_logger.LogInformation("");
var moduleTargetPath = moduleConfiguration.Target is not null
? _pathHelperService.ResolvePath(moduleConfiguration.Target)
: null;
if (moduleTargetPath is not null)
{
_logger.LogInformation($"Target directory is: {moduleTargetPath}");
_logger.LogInformation("");
}
if (moduleConfiguration.Install is not null)
{
_logger.LogInformation("Can be installed.");
}
if (moduleConfiguration.Configure is not null)
{
_logger.LogInformation("Can be configured.");
}
if (moduleConfiguration.Links is { } links && links.Count != 0)
{
var linkCount = links.Count;
_logger.LogInformation("");
_logger.LogInformation($"Has {linkCount} link{(linkCount > 1 ? "s" : "")}:");
_logger.LogInformation("");
foreach (var link in links)
{
var sourcePath = Path.Combine(new FileInfo(moduleConfigFileName!).Directory!.FullName, link.Key);
var sourceExists = File.Exists(sourcePath) || Directory.Exists(sourcePath);
var sourceColor = sourceExists ? ColorCodes.GreenForeground : ColorCodes.RedForeground;
var targetColor = ColorCodes.RedForeground;
if (moduleTargetPath is not null)
{
var targetPath = Path.Combine(moduleTargetPath, link.Key);
var targetExists = File.Exists(targetPath) || Directory.Exists(targetPath);
targetColor = targetExists ? ColorCodes.GreenForeground : ColorCodes.RedForeground;
}
_logger.LogInformation($"{sourceColor}{link.Key}{ColorCodes.Reset} -> {targetColor}{link.Value}");
}
}
else
{
_logger.LogInformation("Has no links.");
}
}
}

View File

@@ -0,0 +1,64 @@
using Alma.Configuration.Repository;
using Alma.Logging;
using Alma.Services;
namespace Alma.Command.Info;
public class ModuleInfoCommand : ICommand
{
public string CommandString => "info";
private readonly IFolderService _folderService;
private readonly IRepositoryConfiguration _repositoryConfiguration;
private readonly ILogger<ModuleInfoCommand> _logger;
public ModuleInfoCommand(
IFolderService folderService,
IRepositoryConfiguration repositoryConfiguration,
ILogger<ModuleInfoCommand> logger
)
{
_folderService = folderService;
_repositoryConfiguration = repositoryConfiguration;
_logger = logger;
}
public Task Run(List<string> parameters)
{
//Add info REPO
//Add info REPO MODULE
_logger.LogInformation("AppData folder: " + _folderService.AppData);
if (_folderService.ConfigRoot is string configRoot)
{
_logger.LogInformation("Configuration folder: " + configRoot);
}
else
{
_logger.LogInformation("Configuration folder not exists.");
_logger.LogInformation("Preffered configuration folder is: " + Path.Combine(_folderService.GetPreferredConfigurationFolder(), _folderService.ApplicationSubfolderName));
}
_logger.LogInformation("");
if (_repositoryConfiguration.Configuration.Repositories is var repositores && repositores?.Count > 0)
{
_logger.LogInformation("Repositories:");
foreach (var repository in repositores)
{
Console.Write(repository.Name);
if (repository.RepositoryPath is not null && !Directory.Exists(repository.RepositoryPath))
{
Console.Write($" (containing folder not exists {repository.RepositoryPath})");
}
_logger.LogInformation("");
}
}
else
{
_logger.LogInformation("No repositories found");
}
return Task.CompletedTask;
}
}

View File

@@ -1,62 +0,0 @@
using Alma.Configuration.Repository;
using Alma.Logging;
using Alma.Services;
namespace Alma.Command.Install;
public class InstallCommand : RepositoryModuleCommandBase
{
private readonly ILogger<InstallCommand> _logger;
private readonly IShellService _shellService;
public override string CommandString => "install";
public override string[] CommandAliases => Array.Empty<string>();
public InstallCommand(
ILogger<InstallCommand> logger,
IRepositoryConfiguration repositoryConfiguration,
IModuleConfigurationResolver moduleConfigurationResolver,
IShellService shellService,
IPathHelperService pathHelperService)
: base(repositoryConfiguration, pathHelperService, moduleConfigurationResolver)
{
_logger = logger;
_shellService = shellService;
}
public override async Task Run(List<string> parameters)
{
var (repoName, moduleName) = GetRepositoryAndModuleName(parameters);
if (moduleName is null)
{
_logger.LogInformation("No module specified");
return;
}
var (moduleConfiguration, _) = await GetModuleConfiguration(repoName, moduleName);
if (moduleConfiguration is null)
{
_logger.LogInformation($"No module configuration found for module '{moduleName}'{(repoName is null ? "" : $" in repository '{repoName}'")}");
return;
}
var installLines = moduleConfiguration.Install?.Split(Environment.NewLine);
if (installLines is null)
{
_logger.LogInformation("No install command is found");
return;
}
_logger.LogInformation($"Install command: {string.Join("\n", installLines)}");
if (installLines.Length == 1)
{
_logger.LogInformation("Running install command '" + installLines[0] + "'");
await _shellService.RunCommandAsync(installLines[0]);
}
else
{
_logger.LogError("Multi line scripts are not currently supported");
}
}
}

View File

@@ -3,65 +3,51 @@ using Alma.Configuration.Module;
using Alma.Configuration.Repository; using Alma.Configuration.Repository;
using Alma.Data; using Alma.Data;
using Alma.Logging; using Alma.Logging;
using Alma.Models;
using Alma.Services; using Alma.Services;
namespace Alma.Command.Link; namespace Alma.Command.Link;
public class LinkCommand : RepositoryModuleCommandBase public class LinkCommand : ICommand
{ {
private readonly IRepositoryConfiguration _repositoryConfiguration; private readonly IRepositoryConfiguration _repositoryConfiguration;
private readonly IModuleConfigurationResolver _moduleConfigurationResolver; private readonly IModuleConfigurationResolver _moduleConfigurationResolver;
private readonly IMetadataHandler _metadataHandler; private readonly IMetadataHandler _metadataHandler;
private readonly IPathHelperService _pathHelperService;
private readonly ILogger<LinkCommand> _logger; private readonly ILogger<LinkCommand> _logger;
public override string CommandString => "link"; public string CommandString => "link";
public override string[] CommandAliases => Array.Empty<string>();
public LinkCommand( public LinkCommand(
IRepositoryConfiguration repositoryConfiguration, IRepositoryConfiguration repositoryConfiguration,
IModuleConfigurationResolver moduleConfigurationResolver, IModuleConfigurationResolver moduleConfigurationResolver,
IMetadataHandler metadataHandler, IMetadataHandler metadataHandler,
IPathHelperService pathHelperService,
ILogger<LinkCommand> logger) ILogger<LinkCommand> logger)
: base(repositoryConfiguration, pathHelperService, moduleConfigurationResolver)
{ {
_repositoryConfiguration = repositoryConfiguration; _repositoryConfiguration = repositoryConfiguration;
_moduleConfigurationResolver = moduleConfigurationResolver; _moduleConfigurationResolver = moduleConfigurationResolver;
_metadataHandler = metadataHandler; _metadataHandler = metadataHandler;
_pathHelperService = pathHelperService;
_logger = logger; _logger = logger;
} }
public override async Task Run(List<string> parameters) public async Task Run(List<string> parameters)
{ {
if (parameters.Contains("--help")) if (parameters.Count == 0)
{
_logger.LogInformation(
"""
Usage:
alma link [module]
alma link [repository] [module]
Options:
--help Show this message
-d, --dry-run Show what would be linked without actually linking
"""
);
return;
}
var dryRun = parameters.Contains("-d") || parameters.Contains("--dry-run");
var (repoName, moduleName) = GetRepositoryAndModuleName(parameters);
if (moduleName is null)
{ {
_logger.LogInformation("No module specified"); _logger.LogInformation("No module specified");
return; return;
} }
var (sourceDirectory, targetDirectory) = GetRepositorySourceAndTargetDirectory(repoName); string moduleName = parameters[0];
string sourceDirectory = Path.Combine(Environment.CurrentDirectory);
string targetDirectory = Path.Combine(Environment.CurrentDirectory, "..");
var repoName = GetRepositoryName(parameters);
if (repoName is not null
&& _repositoryConfiguration.Configuration.Repositories.FirstOrDefault(r => r.Name == repoName) is { } repoConfig)
{
sourceDirectory = repoConfig.RepositoryPath ?? sourceDirectory;
targetDirectory = repoConfig.LinkPath ?? targetDirectory;
}
if (!Directory.Exists(sourceDirectory)) if (!Directory.Exists(sourceDirectory))
{ {
@@ -83,7 +69,7 @@ public class LinkCommand : RepositoryModuleCommandBase
if (moduleConfiguration?.Target is string moduleTargetDir) if (moduleConfiguration?.Target is string moduleTargetDir)
{ {
targetDirectory = _pathHelperService.ResolvePath(moduleTargetDir, targetDirectory); targetDirectory = ResolvePath(moduleTargetDir, targetDirectory);
} }
if (!Directory.Exists(targetDirectory)) if (!Directory.Exists(targetDirectory))
@@ -101,46 +87,14 @@ public class LinkCommand : RepositoryModuleCommandBase
moduleDir, moduleDir,
currentTargetDirectory, currentTargetDirectory,
moduleConfiguration)).ToList(); moduleConfiguration)).ToList();
// Exclude
if (moduleConfigurationFile is not null) itemsToLink.RemoveAll(i => i.SourcePath == moduleConfigurationFileFullPath); if (moduleConfigurationFile is not null) itemsToLink.RemoveAll(i => i.SourcePath == moduleConfigurationFileFullPath);
if (moduleConfiguration?.Exclude is { } excludeList) var successfulLinks = CreateLinks(itemsToLink);
{
foreach (var itemToExclude in excludeList)
{
var excludePath = Path.Combine(moduleDirectory, Path.Combine(itemToExclude.Split('/')));
itemsToLink.RemoveAll(
i => i.SourcePath == excludePath
|| i.SourcePath.StartsWith(excludePath + Path.DirectorySeparatorChar)
);
}
}
if (moduleConfiguration?.ExcludeReadme ?? false)
{
foreach (var readmeFile in Enum.GetValues<ReadmeFiles>())
{
var readmeFilePath = Path.Combine(moduleDirectory, readmeFile.GetFileName());
itemsToLink.RemoveAll(i => i.SourcePath == readmeFilePath);
}
}
// Linking
if (dryRun)
{
_logger.LogInformation("Dry run. No links will be created. The following links would be created:");
}
var successfulLinks = CreateLinks(itemsToLink, dryRun);
if (dryRun) return;
await _metadataHandler.SaveLinkedItemsAsync(successfulLinks, moduleDir, currentTargetDirectory); await _metadataHandler.SaveLinkedItemsAsync(successfulLinks, moduleDir, currentTargetDirectory);
} }
private List<ItemToLink> CreateLinks(List<ItemToLink> itemsToLink, bool dryRun) private List<ItemToLink> CreateLinks(List<ItemToLink> itemsToLink)
{ {
var successfulLinks = new List<ItemToLink>(); var successfulLinks = new List<ItemToLink>();
@@ -159,8 +113,6 @@ public class LinkCommand : RepositoryModuleCommandBase
_logger.LogInformation($"Linking: '{itemToLink.SourcePath}' '{itemToLink.TargetPath}'"); _logger.LogInformation($"Linking: '{itemToLink.SourcePath}' '{itemToLink.TargetPath}'");
if (!dryRun)
{
if (sourceFileExists) if (sourceFileExists)
{ {
File.CreateSymbolicLink(itemToLink.TargetPath, itemToLink.SourcePath); File.CreateSymbolicLink(itemToLink.TargetPath, itemToLink.SourcePath);
@@ -174,7 +126,6 @@ public class LinkCommand : RepositoryModuleCommandBase
_logger.LogInformation("Source not exists: " + itemToLink.SourcePath); _logger.LogInformation("Source not exists: " + itemToLink.SourcePath);
continue; continue;
} }
}
successfulLinks.Add(itemToLink); successfulLinks.Add(itemToLink);
} }
@@ -184,7 +135,7 @@ public class LinkCommand : RepositoryModuleCommandBase
_logger.LogInformation("An error occured while creating links: " + e.Message); _logger.LogInformation("An error occured while creating links: " + e.Message);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
_logger.LogInformation("On Windows symlinks can be created only with Administrator privileges."); _logger.LogInformation("On Windows symlinks can be greated only with Administrator privileges.");
} }
} }
@@ -201,14 +152,7 @@ public class LinkCommand : RepositoryModuleCommandBase
var filesToLink = new List<ItemToLink>(); var filesToLink = new List<ItemToLink>();
foreach (var file in currentDirectory.GetFiles()) foreach (var file in currentDirectory.GetFiles())
{ {
if (moduleConfiguration?.Links?.ContainsKey(file.Name) ?? false) filesToLink.Add(new ItemToLink(Path.Combine(currentDirectory.FullName, file.Name), Path.Combine(currentTargetDirectory.FullName, file.Name)));
{
filesToLink.Add(new ItemToLink(file.FullName, _pathHelperService.ResolvePath(moduleConfiguration.Links[file.Name], targetDirectory.FullName)));
}
else
{
filesToLink.Add(new ItemToLink(file.FullName, Path.Combine(currentTargetDirectory.FullName, file.Name)));
}
} }
var subDirLinksToAdd = Enumerable.Empty<ItemToLink>(); var subDirLinksToAdd = Enumerable.Empty<ItemToLink>();
@@ -218,7 +162,7 @@ public class LinkCommand : RepositoryModuleCommandBase
var relativePath = GetRelativePath(subDir.FullName, moduleDirectory.FullName); var relativePath = GetRelativePath(subDir.FullName, moduleDirectory.FullName);
if (moduleConfiguration?.Links?.ContainsKey(relativePath) ?? false) if (moduleConfiguration?.Links?.ContainsKey(relativePath) ?? false)
{ {
filesToLink.Add(new ItemToLink(subDir.FullName, _pathHelperService.ResolvePath(moduleConfiguration.Links[relativePath], targetDirectory.FullName))); filesToLink.Add(new ItemToLink(subDir.FullName, ResolvePath(moduleConfiguration.Links[relativePath], targetDirectory.FullName)));
} }
else else
{ {
@@ -236,5 +180,24 @@ public class LinkCommand : RepositoryModuleCommandBase
return filesToLink.Concat(subDirLinksToAdd); return filesToLink.Concat(subDirLinksToAdd);
} }
private static string GetRelativePath(string full, string parent) => full[parent.Length..].TrimStart(Path.DirectorySeparatorChar); private static string? GetRepositoryName(List<string> parameters)
{
//TODO: handle parameters
if (parameters.Count < 2) return null;
return parameters[1];
}
private static string ResolvePath(string path, string currentDirectory)
{
if (path.StartsWith("~"))
{
path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), path.Substring(2));
}
//TODO: more special character
return Path.Combine(currentDirectory, path);
}
private static string GetRelativePath(string full, string parent) => full.Substring(parent.Length).TrimStart(Path.DirectorySeparatorChar);
} }

View File

@@ -12,7 +12,6 @@ public class ListCommand : ICommand
private readonly ILogger<ListCommand> _logger; private readonly ILogger<ListCommand> _logger;
public string CommandString => "ls"; public string CommandString => "ls";
public string[] CommandAliases => Array.Empty<string>();
public ListCommand( public ListCommand(
IRepositoryConfiguration repositoryConfiguration, IRepositoryConfiguration repositoryConfiguration,

View File

@@ -1,92 +0,0 @@
using Alma.Command.Install;
using Alma.Configuration.Repository;
using Alma.Logging;
using Alma.Models;
using Alma.Services;
namespace Alma.Command.List;
public class ReadMeCommand : RepositoryModuleCommandBase
{
private ILogger<InstallCommand> _logger;
public override string CommandString => "readme";
public override string[] CommandAliases => Array.Empty<string>();
private readonly Dictionary<ReadmeFiles, Func<string, Task>> _readmeFilePrinters;
public ReadMeCommand(
ILogger<InstallCommand> logger,
IRepositoryConfiguration repositoryConfiguration,
IModuleConfigurationResolver moduleConfigurationResolver,
IPathHelperService pathHelperService)
: base(repositoryConfiguration, pathHelperService, moduleConfigurationResolver)
{
_logger = logger;
_readmeFilePrinters = new Dictionary<ReadmeFiles, Func<string, Task>>
{
{ ReadmeFiles.Markdown, PrintReadMeMd },
{ ReadmeFiles.Text, PrintReadMeText },
{ ReadmeFiles.NoExtension, PrintReadMeText },
};
}
public override async Task Run(List<string> parameters)
{
var (repoName, moduleName) = GetRepositoryAndModuleName(parameters);
if (moduleName is null)
{
_logger.LogInformation("No module specified");
return;
}
var (repoSource, _) = GetRepositorySourceAndTargetDirectory(repoName);
if (repoSource is null)
{
_logger.LogInformation("No repository source found");
return;
}
var fileFound = false;
foreach (var readmeFile in _readmeFilePrinters.Keys)
{
// TODO: make this case insensitive
var readmeFilePath = Path.Combine(repoSource, moduleName, readmeFile.ToString());
if (File.Exists(readmeFilePath))
{
fileFound = true;
await _readmeFilePrinters[readmeFile](readmeFilePath);
break;
}
}
if (!fileFound)
{
_logger.LogInformation("No README file found. Supported formats: README.md, README.txt, README");
}
}
private async Task PrintReadMeMd(string filePath)
{
var content = await File.ReadAllLinesAsync(filePath);
foreach (var line in content)
{
//TODO: Add support for markdown
_logger.LogInformation(line);
}
}
private async Task PrintReadMeText(string filePath)
{
var content = await File.ReadAllLinesAsync(filePath);
foreach (var line in content)
{
_logger.LogInformation(line);
}
}
}

View File

@@ -1,89 +0,0 @@
using Alma.Configuration.Repository;
using Alma.Data;
using Alma.Services;
namespace Alma.Command;
public abstract class RepositoryModuleCommandBase : ICommand
{
private readonly IRepositoryConfiguration _repositoryConfiguration;
private readonly IModuleConfigurationResolver _moduleConfigurationResolver;
private readonly IPathHelperService _pathHelperService;
public abstract string CommandString { get; }
public abstract string[] CommandAliases { get; }
public abstract Task Run(List<string> parameters);
protected RepositoryModuleCommandBase(
IRepositoryConfiguration repositoryConfiguration,
IPathHelperService pathHelperService,
IModuleConfigurationResolver moduleConfigurationResolver)
{
_repositoryConfiguration = repositoryConfiguration;
_pathHelperService = pathHelperService;
_moduleConfigurationResolver = moduleConfigurationResolver;
}
protected async Task<(Configuration.Module.ModuleConfiguration? mergedModuleConfig, string? moduleConfigFileName)> GetModuleConfiguration(string? repoName, string moduleName)
{
var (repoSourceDirectory, _) = GetRepositorySourceAndTargetDirectory(repoName);
var moduleNameAsPath = moduleName.Replace('/', Path.DirectorySeparatorChar);
var moduleDirectory = Path.Combine(repoSourceDirectory, moduleNameAsPath);
var moduleConfigFileStub = Path.Combine(moduleDirectory, Constants.ModuleConfigFileStub);
return await _moduleConfigurationResolver.ResolveModuleConfiguration(moduleConfigFileStub);
}
protected (string? repoName, string? moduleName) GetRepositoryAndModuleName(List<string> parameters, bool singleParamIsRepo = false)
{
//TODO: handle parameters
string? repositoryName = null;
string? moduleName = null;
parameters = parameters.Where(p => !p.StartsWith("-")).ToList();
if (parameters.Count == 1)
{
if (singleParamIsRepo)
{
repositoryName = parameters[0];
}
else
{
moduleName = parameters[0];
}
}
else if (parameters.Count >= 1)
{
repositoryName = parameters[0];
moduleName = parameters[1];
}
return (repositoryName, moduleName);
}
protected (string repoSourceDirectory, string repoTargetDirectory) GetRepositorySourceAndTargetDirectory(string? repoName)
{
string repoSourceDirectory = Path.Combine(Environment.CurrentDirectory);
string repoTargetDirectory = Path.Combine(Environment.CurrentDirectory, "..");
return GetRepositorySourceAndTargetDirectory(repoName, repoSourceDirectory, repoTargetDirectory);
}
protected (string repoSourceDirectory, string repoTargetDirectory) GetRepositorySourceAndTargetDirectory(string? repoName, string fallbackSourceDirectory, string fallbackTargetDirectory)
{
if (repoName is not null
&& _repositoryConfiguration.Configuration.Repositories.Find(r => r.Name == repoName) is { } repoConfig)
{
fallbackSourceDirectory =
repoConfig.RepositoryPath is { } repoPath
? _pathHelperService.ResolvePath(repoPath)
: fallbackSourceDirectory;
fallbackTargetDirectory =
repoConfig.LinkPath is { } linkPath
? _pathHelperService.ResolvePath(linkPath)
: fallbackTargetDirectory;
}
return (fallbackSourceDirectory, fallbackTargetDirectory);
}
}

View File

@@ -3,7 +3,6 @@ namespace Alma.Command.Unlink;
public class UnlinkCommand : ICommand public class UnlinkCommand : ICommand
{ {
public string CommandString => "unlink"; public string CommandString => "unlink";
public string[] CommandAliases => Array.Empty<string>();
public Task Run(List<string> parameters) public Task Run(List<string> parameters)
{ {
throw new NotImplementedException(); throw new NotImplementedException();

View File

@@ -1,20 +0,0 @@
namespace Alma.Models;
public enum ReadmeFiles
{
Markdown,
Text,
NoExtension
}
public static class ReadmeFileTypeExtensions
{
public static string GetFileName(this ReadmeFiles type)
=> type switch
{
ReadmeFiles.Markdown => "README.md",
ReadmeFiles.Text => "README.txt",
ReadmeFiles.NoExtension => "README",
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
}

View File

@@ -19,14 +19,7 @@ public class ConfigurationFileReader
{ {
foreach (var configurationFileReader in _configurationFileReaders) foreach (var configurationFileReader in _configurationFileReaders)
{ {
if (await configurationFileReader.DeserializeAsync<T>( if (await configurationFileReader.DeserializeAsync<T>(fileNameWithoutExtension, contextGenerator, extension) is { Result: { } } result) return result;
fileNameWithoutExtension,
contextGenerator,
extension) is { Result: { } } result
)
{
return result;
}
} }
return (null, null); return (null, null);

View File

@@ -1,70 +1,47 @@
using System.Diagnostics.CodeAnalysis;
using Alma.Command.Diag;
using Alma.Logging;
namespace Alma.Services; namespace Alma.Services;
public class FolderService : IFolderService public class FolderService : IFolderService
{ {
private readonly Dictionary<string, Func<string?>> _configHomeProviders;
private readonly Dictionary<string, Func<string?>> _appDataProviders;
public string? ConfigRoot { get; } public string? ConfigRoot { get; }
public string AppData { get; } public string AppData { get; }
public string ApplicationSubfolderName => "alma"; public string ApplicationSubfolderName => "alma";
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(FolderService))]
public FolderService() public FolderService()
{ {
_configHomeProviders = ConfigHomeProviders();
_appDataProviders = AppDataProviders();
ConfigRoot = GetConfigHomePath(); ConfigRoot = GetConfigHomePath();
AppData = GetAppDataPath(); AppData = GetAppDataPath();
if (!Directory.Exists(AppData)) Directory.CreateDirectory(AppData); if (!Directory.Exists(AppData)) Directory.CreateDirectory(AppData);
} }
private static Dictionary<string, Func<string?>> ConfigHomeProviders() public string GetPreferredConfigurationFolder()
{
return new Dictionary<string, Func<string?>>
{
{"ALMA_CONFIG", () => Environment.GetEnvironmentVariable("ALMA_CONFIG")},
{"XDG_CONFIG_HOME", () => Environment.GetEnvironmentVariable("XDG_CONFIG_HOME")},
{"UserProfile", GetPreferredConfigurationFolder},
{"ALMA_CONFIG_FALLBACK", () => Environment.GetEnvironmentVariable("ALMA_CONFIG_FALLBACK")},
};
}
private static Dictionary<string, Func<string?>> AppDataProviders()
{
return new Dictionary<string, Func<string?>>
{
{"ALMA_APP_DATA", () => Environment.GetEnvironmentVariable("ALMA_APP_DATA")},
{"LocalApplicationData", () => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)},
{"ALMA_APP_DATA_FALLBACK", () => Environment.GetEnvironmentVariable("ALMA_APP_DATA_FALLBACK")},
};
}
string IFolderService.GetPreferredConfigurationFolder()
=> GetPreferredConfigurationFolder();
public static string GetPreferredConfigurationFolder()
=> Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config"); => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config");
private string? GetConfigHomePath() private string? GetConfigHomePath()
{ {
var configHome = EnumerateProviders(_configHomeProviders.Values); var configHomeProviders = new List<Func<string?>>
{
() => Environment.GetEnvironmentVariable("XDG_CONFIG_HOME"),
() => GetPreferredConfigurationFolder()
};
var configHome = EnumerateProviders(configHomeProviders);
return configHome == null ? null : Path.Combine(configHome, ApplicationSubfolderName); return configHome == null ? null : Path.Combine(configHome, ApplicationSubfolderName);
} }
private string GetAppDataPath() private string GetAppDataPath()
{ {
var appData = EnumerateProviders(_appDataProviders.Values) ?? Environment.CurrentDirectory; var appDataProviders = new List<Func<string?>>
{
() => Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)
};
var appData = EnumerateProviders(appDataProviders) ?? Environment.CurrentDirectory;
return Path.Combine(appData, ApplicationSubfolderName); return Path.Combine(appData, ApplicationSubfolderName);
} }
private static string? EnumerateProviders(IEnumerable<Func<string?>> providers) private static string? EnumerateProviders(List<Func<string?>> providers)
{ {
string? result = null; string? result = null;
@@ -76,26 +53,4 @@ public class FolderService : IFolderService
return result; return result;
} }
[DiagnosticHelper("config-home-providers")]
public static void ConfigHomeProviderDiag(ILogger logger)
{
var configHomeProviders = ConfigHomeProviders();
logger.LogInformation($"There are {configHomeProviders.Count} config home providers:");
foreach (var configHome in configHomeProviders)
{
logger.LogInformation($"{configHome.Key} => {configHome.Value() ?? "<null>"}");
}
}
[DiagnosticHelper("app-data-providers")]
public static void AppDataProviderDiag(ILogger logger)
{
var appDataProviders = AppDataProviders();
logger.LogInformation($"There are {appDataProviders.Count} app data providers:");
foreach (var appData in appDataProviders)
{
logger.LogInformation($"{appData.Key} => {appData.Value() ?? "<null>"}");
}
}
} }

View File

@@ -17,12 +17,6 @@ public class JsonConfigurationFileReader : IConfigurationFileReader
if (!File.Exists(fileName)) return (null, null); if (!File.Exists(fileName)) return (null, null);
await using FileStream openStream = File.OpenRead(fileName); await using FileStream openStream = File.OpenRead(fileName);
var result = return ((T?)await JsonSerializer.DeserializeAsync(openStream, typeof(T), contextGenerator(new JsonSerializerOptions(DefaultOptions))), fileName);
(T?)await JsonSerializer.DeserializeAsync(
openStream,
typeof(T),
contextGenerator(new JsonSerializerOptions(DefaultOptions))
);
return (result, fileName);
} }
} }

View File

@@ -21,19 +21,11 @@ public class ModuleConfigurationResolver : IModuleConfigurationResolver
if (moduleConfigRoot is null) return (null, null); if (moduleConfigRoot is null) return (null, null);
var validModuleConfigurations = await moduleConfigRoot var validModuleConfigurations = moduleConfigRoot.Where(m => _osInformation.IsOnPlatform(m.Key));
.ToAsyncEnumerable()
.WhereAwait(async m => await _osInformation.IsOnPlatformAsync(m.Key))
.ToListAsync();
//TODO: priority order //TODO: priority order
var orderedValidModuleConfigurations = new Dictionary<string, ModuleConfiguration>(validModuleConfigurations); var orderedValidModuleConfigurations = new Dictionary<string, ModuleConfiguration>(validModuleConfigurations);
if (orderedValidModuleConfigurations.Count == 0)
{
return (ModuleConfiguration.Empty(), moduleConfigFileName);
}
var mergedModuleConfig = orderedValidModuleConfigurations var mergedModuleConfig = orderedValidModuleConfigurations
.Select(m => m.Value) .Select(m => m.Value)
.Aggregate((a, b) => a.Merge(b)); .Aggregate((a, b) => a.Merge(b));

View File

@@ -6,52 +6,19 @@ public class OsInformation : IOsInformation
{ {
private const string OsIdentifierDefault = "default"; private const string OsIdentifierDefault = "default";
private const string OsIdentifierWin = "windows"; private const string OsIdentifierWin = "windows";
private const string OsIdentifierMac = "macos";
private const string OsIdentifierFreeBsd = "freebsd";
private const string OsIdentifierLinux = "linux"; private const string OsIdentifierLinux = "linux";
private const string LinuxOsRelease = "/etc/os-release"; public string GetOsIdentifier()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return OsIdentifierWin;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return OsIdentifierLinux;
public async Task<string> GetOsIdentifierAsync()
{
string? baseOsIdentifier = null;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) baseOsIdentifier = OsIdentifierWin;
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) baseOsIdentifier = OsIdentifierMac;
else if (RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD)) baseOsIdentifier = OsIdentifierFreeBsd;
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
baseOsIdentifier = OsIdentifierLinux;
try
{
if (File.Exists(LinuxOsRelease))
{
var lines = await File.ReadAllLinesAsync(LinuxOsRelease);
var distroName = lines.FirstOrDefault(l => l.StartsWith("id=", StringComparison.InvariantCultureIgnoreCase));
if (distroName is not null)
{
distroName = distroName.ToLower().Substring(distroName.IndexOf("=", StringComparison.Ordinal) + 1);
baseOsIdentifier += "-" + distroName;
}
}
}
catch
{
}
}
if (baseOsIdentifier is null)
return "unknown"; return "unknown";
var architecture = RuntimeInformation.ProcessArchitecture.ToString().ToLower();
return baseOsIdentifier + "-" + architecture;
} }
public async Task<bool> IsOnPlatformAsync(string platform) public bool IsOnPlatform(string platform)
{ {
return platform == OsIdentifierDefault if (platform == OsIdentifierDefault) return true;
|| (await GetOsIdentifierAsync()).StartsWith(platform, StringComparison.InvariantCultureIgnoreCase); return platform == GetOsIdentifier();
} }
} }

View File

@@ -1,47 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Alma.Command.Diag;
using Alma.Data;
using Alma.Logging;
namespace Alma.Services;
public class PathHelperService : IPathHelperService
{
private static readonly List<SpecialPathResolver> _specialPathResolvers = new()
{
new("~", () => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), true),
new("%DOCUMENTS%", () => Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)),
};
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(PathHelperService))]
public string ResolvePath(string path, string? currentDirectory = null)
{
var skipCombiningCurrentDirectory = false;
foreach (var specialPathResolver in _specialPathResolvers)
{
if (path.Contains(specialPathResolver.PathName))
{
var parts = path.Split(specialPathResolver.PathName);
path = string.Join(specialPathResolver.Resolver(), parts);
skipCombiningCurrentDirectory = (specialPathResolver.SkipCombiningCurrentDirectory ?? false) || skipCombiningCurrentDirectory;
}
}
path = path.Replace('/', Path.DirectorySeparatorChar);
return currentDirectory is null || skipCombiningCurrentDirectory
? path
: Path.Combine(currentDirectory, path);
}
[DiagnosticHelper("special-path-resolver")]
public static void SpecialPathResolverDiag(ILogger logger)
{
logger.LogInformation($"There are {_specialPathResolvers.Count} special path resolvers:");
foreach (var specialPathResolver in _specialPathResolvers)
{
logger.LogInformation($"{specialPathResolver.PathName} => {specialPathResolver.Resolver()}");
}
}
}

View File

@@ -1,80 +0,0 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using Alma.Logging;
namespace Alma.Services;
public class ShellService : IShellService
{
private readonly ILogger<ShellService> _logger;
public ShellService(ILogger<ShellService> logger)
{
_logger = logger;
}
public async Task RunCommandAsync(string command)
{
Process? process;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
process = CreateLinuxShell(command);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
process = CreateWindowsShell(command);
}
else
{
_logger.LogError("Platform not supported");
throw new NotSupportedException();
}
if (!process.Start()) return;
var reader = process.StandardOutput;
while (!reader.EndOfStream)
{
var content = await reader.ReadLineAsync();
if (content is not null)
{
_logger.LogInformation(content);
}
}
await process.WaitForExitAsync();
}
private Process CreateLinuxShell(string command)
{
var processStartInfo = new ProcessStartInfo
{
FileName = "sh",
ArgumentList = {"-c", command},
RedirectStandardOutput = true,
RedirectStandardInput = true,
CreateNoWindow = true,
UseShellExecute = false
};
return new Process {StartInfo = processStartInfo};
}
private Process CreateWindowsShell(string command)
{
var processStartInfo = new ProcessStartInfo
{
//TODO: customizable shell
FileName = "pwsh",
ArgumentList = {"-c", command},
RedirectStandardOutput = true,
RedirectStandardInput = true,
CreateNoWindow = true,
UseShellExecute = false
};
return new Process {StartInfo = processStartInfo};
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>

View File

@@ -1,17 +1,10 @@
namespace Alma.Logging; namespace Alma.Logging;
public interface ILogger public interface ILogger<T>
{ {
LogLevel DefaultLogLevel { get; } LogLevel DefaultLogLevel { get; }
void LogInformation(string logMessage); void LogInformation(string logMessage);
void LogDebug(string logMessage); void LogDebug(string logMessage);
void LogTrace(string logMessage); void LogTrace(string logMessage);
void Log(string logMessage, LogLevel logLevel); void Log(string logMessage, LogLevel logLevel);
void LogError(string logMessage);
void LogCritical(string logMessage);
}
public interface ILogger<T> : ILogger
{
} }

View File

@@ -3,6 +3,5 @@
public interface ILoggerFactory public interface ILoggerFactory
{ {
ILogger<T> CreateLogger<T>(); ILogger<T> CreateLogger<T>();
ILogger CreateLogger(Type t);
LogLevel DefaultLogLevel { get; } LogLevel DefaultLogLevel { get; }
} }

View File

@@ -2,8 +2,6 @@
public enum LogLevel public enum LogLevel
{ {
Critical,
Error,
Information, Information,
Debug, Debug,
Trace Trace

View File

@@ -1,11 +1,10 @@
namespace Alma.Logging; namespace Alma.Logging;
public class Logger<T> : ILogger<T>
public class Logger : ILogger
{ {
public LogLevel DefaultLogLevel { get; } public LogLevel DefaultLogLevel { get; }
public Logger(LogLevel defaultLogLevel, string topicName) public Logger(LogLevel defaultLogLevel)
{ {
DefaultLogLevel = defaultLogLevel; DefaultLogLevel = defaultLogLevel;
} }
@@ -23,22 +22,4 @@ public class Logger : ILogger
Console.WriteLine(s); Console.WriteLine(s);
} }
} }
public void LogError(string logMessage)
{
Log(logMessage, LogLevel.Error);
}
public void LogCritical(string logMessage)
{
Log(logMessage, LogLevel.Critical);
}
}
public class Logger<T> : Logger, ILogger<T>
{
public Logger(LogLevel defaultLogLevel) : base(defaultLogLevel, typeof(T).Name)
{
}
} }

View File

@@ -13,9 +13,4 @@ public class LoggerFactory : ILoggerFactory
{ {
return new Logger<T>(DefaultLogLevel); return new Logger<T>(DefaultLogLevel);
} }
public ILogger CreateLogger(Type t)
{
return new Logger(DefaultLogLevel, t.Name);
}
} }

View File

@@ -6,16 +6,14 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Jab" Version="0.10.2" /> <PackageReference Include="Jab" Version="0.8.4" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<VersionPrefix>0.0.5</VersionPrefix>
<VersionSuffix>development</VersionSuffix>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@@ -1,9 +1,5 @@
using Alma.Command; using Alma.Command;
using Alma.Command.Configure;
using Alma.Command.Diag;
using Alma.Command.Help;
using Alma.Command.Info; using Alma.Command.Info;
using Alma.Command.Install;
using Alma.Command.Link; using Alma.Command.Link;
using Alma.Command.List; using Alma.Command.List;
using Alma.Command.Unlink; using Alma.Command.Unlink;
@@ -16,18 +12,46 @@ namespace Alma;
public static class Program public static class Program
{ {
/*public static async Task Main(string[] args)
{
var services = BuildServices();
var repositoryConfiguration = services.GetRequiredService<IRepositoryConfiguration>();
await repositoryConfiguration.LoadAsync();
var application = services.GetRequiredService<Application>();
await application.Run(args);
static IServiceProvider BuildServices()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<IRepositoryConfiguration, RepositoryConfiguration>();
serviceCollection.AddSingleton<IFolderService, FolderService>();
serviceCollection.AddSingleton<ConfigurationFileReader>();
serviceCollection.AddSingleton<IConfigurationFileReader, JsonConfigurationFileReader>();
serviceCollection.AddSingleton<IOsInformation, OsInformation>();
serviceCollection.AddSingleton<ICommand, LinkCommand>();
serviceCollection.AddSingleton<IModuleConfigurationResolver, ModuleConfigurationResolver>();
serviceCollection.AddSingleton<Application>();
typeof(IRepositoryConfiguration), typeof(RepositoryConfiguration)
typeof(IFolderService), typeof(FolderService)
typeof(ConfigurationFileReader)
typeof(IConfigurationFileReader), typeof(JsonConfigurationFileReader)
typeof(IOsInformation), typeof(OsInformation)
typeof(ICommand), typeof(LinkCommand)
typeof(IModuleConfigurationResolver), typeof(ModuleConfigurationResolver)
typeof(Application)
return serviceCollection.BuildServiceProvider();
}
}*/
public static async Task Main(string[] args) public static async Task Main(string[] args)
{ {
InitLogging(); InitLogging();
var logger = AlmaLoggerFactory.CreateLogger(typeof(Program));
var workdir = GetWorkdir(logger);
if (workdir != null)
{
Environment.CurrentDirectory = workdir;
}
var services = new AlmaServiceProvider(); var services = new AlmaServiceProvider();
var repositoryConfiguration = services.GetService<IRepositoryConfiguration>(); var repositoryConfiguration = services.GetService<IRepositoryConfiguration>();
@@ -37,39 +61,14 @@ public static class Program
await application.Run(args); await application.Run(args);
} }
private static string? GetWorkdir(ILogger logger) private static ILoggerFactory InitLogging()
{ {
var workdirProviders = new Dictionary<string, Func<string?>> var loggerFactory = new LoggerFactory();
{
{"ALMA_WORKDIR", () => Environment.GetEnvironmentVariable("ALMA_WORKDIR")},
{"WORKDIR", () => Environment.GetEnvironmentVariable("WORKDIR")},
};
foreach (var workdirProvider in workdirProviders) return AlmaLoggerFactory = loggerFactory;
{
var workdir = workdirProvider.Value();
if (workdir != null)
{
if (Directory.Exists(workdir))
{
return workdir;
}
else
{
logger.LogInformation($"{workdirProvider.Key} is set to {workdir} but this directory does not exist.");
}
}
} }
return null; public static ILoggerFactory AlmaLoggerFactory { get; private set; }
}
private static void InitLogging()
{
AlmaLoggerFactory = new LoggerFactory();
}
public static ILoggerFactory AlmaLoggerFactory { get; private set; } = null!;
} }
[ServiceProvider] [ServiceProvider]
@@ -80,18 +79,12 @@ public static class Program
[Singleton(typeof(IOsInformation), typeof(OsInformation))] [Singleton(typeof(IOsInformation), typeof(OsInformation))]
[Singleton(typeof(ICommand), typeof(LinkCommand))] [Singleton(typeof(ICommand), typeof(LinkCommand))]
[Singleton(typeof(ICommand), typeof(UnlinkCommand))] [Singleton(typeof(ICommand), typeof(UnlinkCommand))]
[Singleton(typeof(ICommand), typeof(InfoCommand))] [Singleton(typeof(ICommand), typeof(ModuleInfoCommand))]
[Singleton(typeof(ICommand), typeof(ListCommand))] [Singleton(typeof(ICommand), typeof(ListCommand))]
[Singleton(typeof(ICommand), typeof(InstallCommand))] //Dependency cycle
[Singleton(typeof(ICommand), typeof(HelpCommand))] //[Singleton(typeof(ICommand), typeof(HelpCommand))]
[Singleton(typeof(ICommand), typeof(ConfigureCommand))]
[Singleton(typeof(ICommand), typeof(DiagCommand))]
[Singleton(typeof(ICommand), typeof(ReadMeCommand))]
[Singleton(typeof(IModuleConfigurationResolver), typeof(ModuleConfigurationResolver))] [Singleton(typeof(IModuleConfigurationResolver), typeof(ModuleConfigurationResolver))]
[Singleton(typeof(IMetadataHandler), typeof(MetadataHandler))] [Singleton(typeof(IMetadataHandler), typeof(MetadataHandler))]
[Singleton(typeof(IShellService), typeof(ShellService))]
[Singleton(typeof(IVersionService), typeof(VersionService))]
[Singleton(typeof(IPathHelperService), typeof(PathHelperService))]
[Singleton(typeof(Application))] [Singleton(typeof(Application))]
[Transient(typeof(ILogger<>), Factory = nameof(CustomLoggerFactory))] [Transient(typeof(ILogger<>), Factory = nameof(CustomLoggerFactory))]
internal partial class AlmaServiceProvider internal partial class AlmaServiceProvider

View File

@@ -1,15 +0,0 @@
using System.Reflection;
using Alma.Services;
namespace Alma;
public class VersionService : IVersionService
{
public string GetVersion()
{
return
typeof(Program).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion
?? typeof(Program).Assembly.GetName().Version?.ToString()
?? "unknown";
}
}