Innovenergy_trunk/csharp/app/VenusFirmwareCiDaemon/src/Build.cs

295 lines
8.7 KiB
C#

using System.Text.RegularExpressions;
using CliWrap;
using InnovEnergy.Lib.Utils;
using static InnovEnergy.Server.FirmwareCiDaemon.Logger;
using static InnovEnergy.Server.FirmwareCiDaemon.ExitException;
namespace InnovEnergy.Server.FirmwareCiDaemon;
public record Build
{
private static readonly Regex RxTimestamp = new Regex(@"\d{14}");
private static readonly Regex RxVersion = new Regex(@"v\d+\.\d+(~\w+)?");
private const Int32 MaxPublishedSwus = 12;
public Device Device { get; init; }
public String VeVersion { get; init; }
public Branch Branch { get; init; }
public String Comment { get; init; }
public String Hash { get; init; }
public String ShortHash => Hash[..6];
public Channel Channel => Branch switch
{
Branch.Victron => Channel.Testing,
Branch.Release => Channel.Release,
Branch.Develop => Channel.Develop,
_ => throw new Exception($"Unsupported branch: {Branch}")
};
public String VersionId => Branch switch
{
Branch.Victron => $"{VeVersion}~victron",
Branch.Release => $"{VeVersion}~{Comment}",
Branch.Develop => $"{VeVersion}~{ShortHash}",
_ => throw new Exception($"Unsupported branch: {Branch}")
};
public static Build Prepare(CommitInfo ie, (Device device, String veVersion) ve)
{
return new Build
{
Device = ve.device,
VeVersion = ve.veVersion,
Branch = ie.Branch,
Comment = ie.Comment,
Hash = ie.Hash
};
}
public void Execute()
{
Log();
Log("======================================================");
Log("Starting build");
Log("======================================================");
Log();
Log($"VE Version: {VeVersion}");
Log($"Channel : {Channel}");
Log($"Device : {Device}");
Log($"Branch : {Branch}");
Log($"Comment : {Comment}");
Log($"Version ID: {VersionId}");
Log($"SWU Name : {Device.SwuName()}");
Log($"Hash : {Hash}");
Log();
Log("======================================================");
Log();
using var veBaseSwuFile = FwSource.Victron.GetLatestSwuPath(Channel.Release, Device).Apply(DownloadFile);
using var ieRepo = Fossil.Checkout(Branch);
using var releaseSwu = MergeSwu(veBaseSwuFile, ieRepo.FirmwareDirectory, Device, VersionId);
var publishedSwu = PublishSwu(Device, Channel, releaseSwu);
RemoveOldFiles(Device, Channel);
var zipPath = Channel.ZipPath();
var removeGlob = Device.SwuBase() + "*";
if (zipPath is null)
return;
Log($"Updating {zipPath[Program.IeBasePath.Length..]}");
zipPath.RemoveFromZip(removeGlob);
zipPath.AddToZip(publishedSwu);
}
public static Disposable<String> DownloadFile(String url)
{
var fileName = Path.GetFileName(url);
var downloadedFile = FileSystem.CreateTempFile(fileName);
Log($"downloading {url}");
var curl = Cli
.Wrap("curl")
.WithArguments(url)
.PipeToFile(downloadedFile);
if (curl.exitCode != 0)
Exit("Failed to download " + url);
return downloadedFile;
}
private static void UpdateVersionFile(String fwDir, String version, String timestamp, String file)
{
Log($"updating {file}");
var fwFile = fwDir.AppendPath(file);
if (!FileSystem.Local.FileExists(fwFile))
Exit($"Cannot find {fwFile}");
var contents = File.ReadAllText(fwFile);
contents = RxTimestamp.Replace(contents, timestamp);
contents = RxVersion.Replace(contents, version);
try
{
File.WriteAllText(fwFile, contents);
}
catch
{
Exit($"Failed to write to {fwFile}");
}
}
private static void PatchIeFiles(String fossilDir, String mountDir)
{
Log("applying changes");
var (exitCode, stdOut, stdErr) = Cli
.Wrap("rsync")
.WithArguments($"-r -t -v -l -i -u -I {fossilDir}/ {mountDir}") // that / is important!
.ExecuteSync();
if (exitCode != 0)
Exit($"Failed to apply changes!\n{stdErr}\n{stdOut}");
}
private static Disposable<String> Unzip(String ext4GzFile)
{
var ext4GzFileName = Path.GetFileName(ext4GzFile);
Log($"extracting {ext4GzFileName}");
var ext4File = ext4GzFileName
.RemoveSuffix(".gz")
.Apply(FileSystem.CreateTempFile);
var zcat = Cli
.Wrap("zcat")
.WithArguments(ext4GzFile)
.PipeToFile(ext4File);
if (zcat.exitCode != 0)
{
ext4File.Dispose();
Exit("Failed to extract " + ext4GzFile);
}
return ext4File;
}
private static Disposable<String> Mount(String ext4File)
{
var ext4FileName = Path.GetFileName(ext4File);
Log($"mounting {ext4FileName}");
var mountDir = FileSystem.CreateTmpDir();
var mount = Cli
.Wrap("mount")
.WithArguments($"-o loop -t ext4 {ext4File} {mountDir}")
.ExecuteSync();
if (mount.exitCode != 0)
{
Log($"\nFailed to mount {ext4File} on {mountDir}:\n{mount.stdErr}");
mountDir.Dispose();
Exit($"\nFailed to mount {ext4File}:\n{mount.stdErr}");
}
return mountDir.BeforeDisposeDo(Unmount);
void Unmount()
{
Log($"unmounting {ext4FileName}");
var umount = Cli
.Wrap("umount")
.WithArguments(mountDir)
.ExecuteSync();
if (umount.exitCode != 0)
Exit($"Failed to unmount {mountDir}\n{umount.stdErr}");
}
}
private static void ApplyChanges(String ieFirmwareDir,
String ext4File,
String timestamp,
String version)
{
using var mountDir = Mount(ext4File); // IMPORTANT: must unmount (dispose) before zipping ext4 file again!
PatchIeFiles(ieFirmwareDir, mountDir);
UpdateVersionFile(mountDir, version, timestamp, "/etc/issue");
UpdateVersionFile(mountDir, version, timestamp, "/etc/issue.net");
UpdateVersionFile(mountDir, version, timestamp, "/etc/version");
UpdateVersionFile(mountDir, version, timestamp, "/opt/victronenergy/version");
}
private static void UpdateSymlink(String newSwuFile, String newSwuLink)
{
var ln = Cli
.Wrap("ln")
.WithArguments($"-sfn {newSwuFile} {newSwuLink}")
.ExecuteSync();
if (ln.exitCode != 0)
Exit($"failed to update symlink {newSwuLink}");
}
private static void RemoveOldFiles(Device device, Channel channel)
{
var oldFiles = Directory
.GetFiles(FwSource.InnovEnergy.GetDirectory(channel, device))
.Where(f => f.EndsWith(".swu"))
.OrderByDescending(File.GetCreationTimeUtc)
.Skip(MaxPublishedSwus);
foreach (var file in oldFiles)
{
Log($"Deleting old swu file {file.Substring(Program.IeBasePath.Length)}");
File.Delete(file);
}
}
private static String PublishSwu(Device device, Channel channel, String releaseSwu)
{
var swuFileName = Path.GetFileName(releaseSwu);
var newSwuFile = FwSource.InnovEnergy.GetDirectory(channel, device).AppendPath(swuFileName);
var newSwuLink = FwSource.InnovEnergy.GetLatestSwuPath(channel, device);
Log($"publishing {newSwuFile[Program.IeBasePath.Length..]}");
new FileInfo(newSwuFile).Directory?.Create(); // create dir if not exits
File.Move(releaseSwu, newSwuFile);
UpdateSymlink(newSwuFile, newSwuLink);
return newSwuFile;
}
private static Disposable<String> MergeSwu(String victronBaseSwuFile,
String ieFirmwareDir,
Device device,
String version)
{
var timestamp = DateTime.Now.ToString("yyyyMMddHHmmss");
using var cpioDir = Cpio.Extract(victronBaseSwuFile);
var ext4GzFile = cpioDir.Value.AppendPath(device.ExtGzFileName());
using var ext4File = Unzip(ext4GzFile);
UpdateVersionFile(cpioDir, version, timestamp, "/sw-description");
ApplyChanges(ieFirmwareDir, ext4File, timestamp, version);
Zip.GZip(ext4File, ext4GzFile);
var mergedSwuFile = device
.LongSwuFileName(timestamp, version)
.Apply(FileSystem.CreateTempFile);
Cpio.Write(cpioDir, mergedSwuFile);
return mergedSwuFile;
}
}