295 lines
8.7 KiB
C#
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;
|
|
}
|
|
} |