26 Commits

Author SHA1 Message Date
f5f01dd100 InfoCommand repo & module info 2022-12-23 13:59:07 +01:00
a47f9b0826 Move PathHelper to service, ConigureCommand, %DOCUMENTS% 2022-12-23 11:54:20 +01:00
acbae0d18f 📝 Readme Linux installation 2022-12-19 20:50:29 +01:00
68b00169d1 Remove envvar and file content logging 2022-12-19 20:33:12 +01:00
ca28c26366 Remove "Test" from release name 2022-12-19 20:30:45 +01:00
8d0dfd365e Merge branch 'feature/github-actions' 2022-12-19 20:24:43 +01:00
3a0d5fea9a Add versioning to Info command 2022-12-19 20:24:35 +01:00
218a290e10 Get version from tag 2022-12-19 20:13:39 +01:00
c1c11e9b4f Print environment variables 2022-12-19 20:13:24 +01:00
12e65f2f4e Add version scripts 2022-12-19 20:13:24 +01:00
5ae66d2388 Release test 2022-12-19 20:13:24 +01:00
8f3f55c701 Output name for executable 2022-12-19 20:13:24 +01:00
90ca560ad3 Release depends on build 2022-12-19 20:13:24 +01:00
2e90190a97 Release 2022-12-19 20:13:24 +01:00
525699ef1f Add only executable to the artifact 2022-12-19 20:13:24 +01:00
d31bdbda40 Publish Aot 2022-12-19 20:13:24 +01:00
89438a6935 Fix build path 2022-12-19 20:13:24 +01:00
ad65c61927 Fix dotnet RID 2022-12-19 20:13:24 +01:00
ddb452893d Dotnet publish, publish artifact 2022-12-19 20:13:24 +01:00
4d7d12fe5f CI test 2022-12-19 20:13:24 +01:00
e75f853e6a Use IServiceProvider in Help command 2022-12-19 18:56:39 +01:00
b33568cd67 Dotnet 7, publish scripts 2022-11-16 13:26:17 +01:00
dcdda76488 DevContainer 2022-11-16 13:26:17 +01:00
b23ab518db Windows shell 2022-11-08 22:21:00 +01:00
30c3266e25 Install commands, improvements 2022-11-06 18:36:04 +01:00
Ádám Kovács
8fd6b526f8 Fix indexofoutrange when Path is only ~ 2022-11-03 21:59:40 +01:00
37 changed files with 825 additions and 170 deletions

View File

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

74
.github/workflows/github-actions.yml vendored Normal file
View File

@@ -0,0 +1,74 @@
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
steps:
- uses: actions/checkout@v3
- name: Setup dotnet
uses: actions/setup-dotnet@v3
with:
dotnet-version: '7.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/
- 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
- 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*

12
.scripts/versioning.ps1 Normal file
View File

@@ -0,0 +1,12 @@
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

13
.scripts/versioning.sh Executable file
View File

@@ -0,0 +1,13 @@
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

View File

@@ -0,0 +1,12 @@
# 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
```

2
publish-alpine.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/sh
dotnet publish src/Alma/Alma.csproj -c Release -r linux-musl-x64 -p:PublishAot=true

2
publish-linux.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/sh
dotnet publish src/Alma/Alma.csproj -c Release -r linux-x64 -p:PublishAot=true

2
publish-windows.cmd Normal file
View File

@@ -0,0 +1,2 @@
dotnet publish src/Alma/Alma.csproj -c Release -r win-x64 -p:PublishAot=true

View File

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

View File

@@ -5,10 +5,15 @@ public class ModuleConfiguration
public string? Target { get; set; }
public Dictionary<string, string>? Links { get; set; }
public ModuleConfiguration(string? target, Dictionary<string, string>? links)
public string? Install { get; set; }
public string? Configure { get; set; }
public ModuleConfiguration(string? target, Dictionary<string, string>? links, string? install, string? configure)
{
Target = target;
Links = links;
Install = install;
Configure = configure;
}
public ModuleConfiguration Merge(ModuleConfiguration merge)
@@ -17,10 +22,12 @@ public class ModuleConfiguration
.Concat(merge.Links ?? new Dictionary<string, string>());
return new ModuleConfiguration(
merge.Target ?? Target,
new Dictionary<string, string>(mergedLinks)
new Dictionary<string, string>(mergedLinks),
merge.Install ?? Install,
merge.Configure ?? Configure
);
}
public static ModuleConfiguration Empty() =>
new(null, new Dictionary<string, string>());
new(null, new Dictionary<string, string>(), null, null);
}

View File

@@ -0,0 +1,8 @@
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

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

View File

@@ -0,0 +1,3 @@
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
{
string GetOsIdentifier();
bool IsOnPlatform(string platform);
Task<string> GetOsIdentifierAsync();
Task<bool> IsOnPlatformAsync(string platform);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,9 +9,9 @@ public class Application
private readonly IList<ICommand> _commands;
private readonly ILogger<Application> _logger;
public Application(IEnumerable<ICommand> commands, ILogger<Application> logger, ILogger<HelpCommand> helpCommandLogger)
public Application(IEnumerable<ICommand> commands, ILogger<Application> logger)
{
_commands = commands.Append(new HelpCommand(() => _commands!, helpCommandLogger)).ToList();
_commands = commands.ToList();
_logger = logger;
}

View File

@@ -0,0 +1,63 @@
using Alma.Command.Install;
using Alma.Configuration.Repository;
using Alma.Data;
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 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

@@ -10,11 +10,11 @@ public class HelpCommand : ICommand
public string CommandString => "help";
public HelpCommand(
Func<IEnumerable<ICommand>> commandsProvider,
IServiceProvider serviceProvider,
ILogger<HelpCommand> logger
)
{
_commandsProvider = commandsProvider;
_commandsProvider = () => (IEnumerable<ICommand>?)serviceProvider.GetService(typeof(IEnumerable<ICommand>)) ?? throw new ApplicationException();
_logger = logger;
}

View File

@@ -0,0 +1,216 @@
using Alma.Configuration.Module;
using Alma.Configuration.Repository;
using Alma.Data;
using Alma.Helper;
using Alma.Logging;
using Alma.Services;
namespace Alma.Command.Info;
public class InfoCommand : RepositoryModuleCommandBase
{
public override string CommandString => "info";
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();
}
}
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");
}
}
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.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

@@ -1,64 +0,0 @@
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

@@ -0,0 +1,63 @@
using System.Diagnostics;
using Alma.Configuration.Repository;
using Alma.Data;
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 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

@@ -2,52 +2,47 @@ using System.Runtime.InteropServices;
using Alma.Configuration.Module;
using Alma.Configuration.Repository;
using Alma.Data;
using Alma.Helper;
using Alma.Logging;
using Alma.Services;
namespace Alma.Command.Link;
public class LinkCommand : ICommand
public class LinkCommand : RepositoryModuleCommandBase
{
private readonly IRepositoryConfiguration _repositoryConfiguration;
private readonly IModuleConfigurationResolver _moduleConfigurationResolver;
private readonly IMetadataHandler _metadataHandler;
private readonly IPathHelperService _pathHelperService;
private readonly ILogger<LinkCommand> _logger;
public string CommandString => "link";
public override string CommandString => "link";
public LinkCommand(
IRepositoryConfiguration repositoryConfiguration,
IModuleConfigurationResolver moduleConfigurationResolver,
IMetadataHandler metadataHandler,
IPathHelperService pathHelperService,
ILogger<LinkCommand> logger)
: base(repositoryConfiguration, pathHelperService, moduleConfigurationResolver)
{
_repositoryConfiguration = repositoryConfiguration;
_moduleConfigurationResolver = moduleConfigurationResolver;
_metadataHandler = metadataHandler;
_pathHelperService = pathHelperService;
_logger = logger;
}
public async Task Run(List<string> parameters)
public override async Task Run(List<string> parameters)
{
if (parameters.Count == 0)
var (repoName, moduleName) = GetRepositoryAndModuleName(parameters);
if (moduleName is null)
{
_logger.LogInformation("No module specified");
return;
}
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;
}
var (sourceDirectory, targetDirectory) = GetRepositorySourceAndTargetDirectory(repoName);
if (!Directory.Exists(sourceDirectory))
{
@@ -69,7 +64,7 @@ public class LinkCommand : ICommand
if (moduleConfiguration?.Target is string moduleTargetDir)
{
targetDirectory = ResolvePath(moduleTargetDir, targetDirectory);
targetDirectory = _pathHelperService.ResolvePath(moduleTargetDir, targetDirectory);
}
if (!Directory.Exists(targetDirectory))
@@ -135,7 +130,7 @@ public class LinkCommand : ICommand
_logger.LogInformation("An error occured while creating links: " + e.Message);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
_logger.LogInformation("On Windows symlinks can be greated only with Administrator privileges.");
_logger.LogInformation("On Windows symlinks can be created only with Administrator privileges.");
}
}
@@ -162,7 +157,7 @@ public class LinkCommand : ICommand
var relativePath = GetRelativePath(subDir.FullName, moduleDirectory.FullName);
if (moduleConfiguration?.Links?.ContainsKey(relativePath) ?? false)
{
filesToLink.Add(new ItemToLink(subDir.FullName, ResolvePath(moduleConfiguration.Links[relativePath], targetDirectory.FullName)));
filesToLink.Add(new ItemToLink(subDir.FullName, _pathHelperService.ResolvePath(moduleConfiguration.Links[relativePath], targetDirectory.FullName)));
}
else
{
@@ -180,24 +175,5 @@ public class LinkCommand : ICommand
return filesToLink.Concat(subDirLinksToAdd);
}
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);
private static string GetRelativePath(string full, string parent) => full[parent.Length..].TrimStart(Path.DirectorySeparatorChar);
}

View File

@@ -0,0 +1,85 @@
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 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;
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

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

View File

@@ -6,19 +6,52 @@ public class OsInformation : IOsInformation
{
private const string OsIdentifierDefault = "default";
private const string OsIdentifierWin = "windows";
private const string OsIdentifierMac = "macos";
private const string OsIdentifierFreeBsd = "freebsd";
private const string OsIdentifierLinux = "linux";
public string GetOsIdentifier()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return OsIdentifierWin;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return OsIdentifierLinux;
private const string LinuxOsRelease = "/etc/os-release";
return "unknown";
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
{
}
}
public bool IsOnPlatform(string platform)
if (baseOsIdentifier is null)
return "unknown";
var architecture = RuntimeInformation.ProcessArchitecture.ToString().ToLower();
return baseOsIdentifier + "-" + architecture;
}
public async Task<bool> IsOnPlatformAsync(string platform)
{
if (platform == OsIdentifierDefault) return true;
return platform == GetOsIdentifier();
return platform == OsIdentifierDefault
|| (await GetOsIdentifierAsync()).StartsWith(platform, StringComparison.InvariantCultureIgnoreCase);
}
}

View File

@@ -0,0 +1,33 @@
using Alma.Data;
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)),
};
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);
}
}

View File

@@ -0,0 +1,80 @@
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">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

View File

@@ -7,4 +7,6 @@ public interface ILogger<T>
void LogDebug(string logMessage);
void LogTrace(string logMessage);
void Log(string logMessage, LogLevel logLevel);
void LogError(string logMessage);
void LogCritical(string logMessage);
}

View File

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

View File

@@ -22,4 +22,14 @@ public class Logger<T> : ILogger<T>
Console.WriteLine(s);
}
}
public void LogError(string logMessage)
{
Log(logMessage, LogLevel.Error);
}
public void LogCritical(string logMessage)
{
Log(logMessage, LogLevel.Critical);
}
}

View File

@@ -11,9 +11,11 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<VersionPrefix>0.0.0</VersionPrefix>
<VersionSuffix>development</VersionSuffix>
</PropertyGroup>
</Project>

View File

@@ -1,5 +1,8 @@
using Alma.Command;
using Alma.Command.Configure;
using Alma.Command.Help;
using Alma.Command.Info;
using Alma.Command.Install;
using Alma.Command.Link;
using Alma.Command.List;
using Alma.Command.Unlink;
@@ -12,42 +15,6 @@ namespace Alma;
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)
{
InitLogging();
@@ -61,14 +28,12 @@ public static class Program
await application.Run(args);
}
private static ILoggerFactory InitLogging()
private static void InitLogging()
{
var loggerFactory = new LoggerFactory();
return AlmaLoggerFactory = loggerFactory;
AlmaLoggerFactory = new LoggerFactory();
}
public static ILoggerFactory AlmaLoggerFactory { get; private set; }
public static ILoggerFactory AlmaLoggerFactory { get; private set; } = null!;
}
[ServiceProvider]
@@ -79,12 +44,16 @@ public static class Program
[Singleton(typeof(IOsInformation), typeof(OsInformation))]
[Singleton(typeof(ICommand), typeof(LinkCommand))]
[Singleton(typeof(ICommand), typeof(UnlinkCommand))]
[Singleton(typeof(ICommand), typeof(ModuleInfoCommand))]
[Singleton(typeof(ICommand), typeof(InfoCommand))]
[Singleton(typeof(ICommand), typeof(ListCommand))]
//Dependency cycle
//[Singleton(typeof(ICommand), typeof(HelpCommand))]
[Singleton(typeof(ICommand), typeof(InstallCommand))]
[Singleton(typeof(ICommand), typeof(HelpCommand))]
[Singleton(typeof(ICommand), typeof(ConfigureCommand))]
[Singleton(typeof(IModuleConfigurationResolver), typeof(ModuleConfigurationResolver))]
[Singleton(typeof(IMetadataHandler), typeof(MetadataHandler))]
[Singleton(typeof(IShellService), typeof(ShellService))]
[Singleton(typeof(IVersionService), typeof(VersionService))]
[Singleton(typeof(IPathHelperService), typeof(PathHelperService))]
[Singleton(typeof(Application))]
[Transient(typeof(ILogger<>), Factory = nameof(CustomLoggerFactory))]
internal partial class AlmaServiceProvider

View File

@@ -0,0 +1,15 @@
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";
}
}