Skip to content

Commit b19dec9

Browse files
authored
Merge branch 'master' into precursorFragmentGroup
2 parents daa16ae + 615fcba commit b19dec9

31 files changed

+9727
-313
lines changed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
name: Release — Patch Version & Build MSI
2+
3+
on:
4+
release:
5+
types: [released, edited]
6+
7+
jobs:
8+
patch-build-upload:
9+
runs-on: windows-latest
10+
timeout-minutes: 60
11+
12+
steps:
13+
- name: Checkout release tag
14+
uses: actions/checkout@v4
15+
with:
16+
fetch-depth: 0
17+
ref: ${{ github.event.release.tag_name }}
18+
19+
- name: Set up .NET
20+
uses: actions/setup-dotnet@v4
21+
with:
22+
dotnet-version: 8.0.x
23+
24+
- name: Compute version values from tag
25+
id: vers
26+
shell: pwsh
27+
run: |
28+
$tag = "${{ github.event.release.tag_name }}".Trim()
29+
if (-not $tag) { Write-Error "No tag found on release event."; exit 1 }
30+
if ($tag.StartsWith('v')) { $tag = $tag.Substring(1) }
31+
32+
# csproj <Version> can keep pre-release labels
33+
$packageVersion = $tag
34+
35+
# AssemblyVersion/FileVersion must be numeric (no -beta). Use the numeric core.
36+
$numericCore = ($packageVersion -split '[-+]')[0]
37+
$parts = $numericCore.Split('.')
38+
if ($parts.Count -lt 3) {
39+
Write-Error "Tag must be at least Major.Minor.Patch (e.g., v1.2.3). Got: $tag"
40+
exit 1
41+
}
42+
$fileVersion = if ($parts.Count -eq 3) { "$numericCore.0" } else { $numericCore }
43+
44+
"PACKAGE_VERSION=$packageVersion" | Out-File -FilePath $env:GITHUB_ENV -Append
45+
"FILE_VERSION=$fileVersion" | Out-File -FilePath $env:GITHUB_ENV -Append
46+
47+
Write-Host "PackageVersion: $packageVersion"
48+
Write-Host "FileVersion : $fileVersion"
49+
50+
- name: Patch <Version> in EngineLayer.csproj
51+
shell: pwsh
52+
run: |
53+
$csprojPath = "./MetaMorpheus/EngineLayer/EngineLayer.csproj"
54+
if (!(Test-Path $csprojPath)) { Write-Error "Missing $csprojPath"; exit 1 }
55+
56+
[xml]$xml = Get-Content $csprojPath
57+
58+
# Try to find <Version> with and without the MSBuild 2003 namespace
59+
$versionNode = $xml.SelectSingleNode('//Version')
60+
$versionNode.InnerText = $env:PACKAGE_VERSION
61+
$xml.Save($csprojPath)
62+
Write-Host "Set <Version> to $env:PACKAGE_VERSION in $csprojPath"
63+
64+
- name: Set version in AssemblyInfo.cs files
65+
uses: secondbounce/assemblyinfo-update@v2
66+
with:
67+
version: ${{ env.FILE_VERSION }}
68+
directory: './MetaMorpheus/GUI/Properties/'
69+
70+
- name: Restore dependencies
71+
run: dotnet restore ./MetaMorpheus/MetaMorpheus.sln
72+
73+
- name: Build solution
74+
run: dotnet msbuild -v:minimal -p:Configuration=Release -p:UseSharedCompilation=false ./MetaMorpheus/
75+
76+
- name: Build installer
77+
run: |
78+
msbuild ./MetaMorpheus/MetaMorpheusSetup/MetaMorpheusSetup.wixproj /p:Configuration=Release /verbosity=minimal /p:UseSharedCompilation=false
79+
msbuild ./MetaMorpheus/Bootstrapper/Bootstrapper.wixproj /p:Configuration=Release /p:UseSharedCompilation=false
80+
81+
- name: Collect MSI
82+
shell: pwsh
83+
run: |
84+
Copy-Item .\MetaMorpheus\MetaMorpheusSetup\bin\Release\MetaMorpheusInstaller.msi .\MetaMorpheusInstaller.msi
85+
if (!(Test-Path .\MetaMorpheusInstaller.msi)) { Write-Error "MSI not found after build"; exit 1 }
86+
87+
- name: Zip command-line version
88+
run: |
89+
7z a MetaMorpheus_CommandLine.zip .\MetaMorpheus\CMD\bin\Release\net8.0\* "-x!*.xml"
90+
91+
# check that installer is greater than 1 kb to avoid pushing an empty artifact
92+
- name: Validate command-line zip artifact
93+
run: |
94+
if ((Get-Item MetaMorpheus_CommandLine.zip).length -lt 1kb) {
95+
Write-Host "❌ The build failed because the command-line .zip did not build properly; it is empty." -ForegroundColor Red
96+
exit 1
97+
}
98+
99+
- name: Release
100+
uses: softprops/action-gh-release@v2
101+
if: github.ref_type == 'tag'
102+
with:
103+
files: |
104+
MetaMorpheus_CommandLine.zip
105+
MetaMorpheusInstaller.msi

.github/workflows/InstallRunAndArtifact.yml

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,6 @@ jobs:
1818
steps:
1919
- uses: actions/checkout@v4
2020

21-
- name: Debug secrets (PowerShell)
22-
shell: pwsh
23-
run: |
24-
if (-not $env:KEY_SECRET) {
25-
Write-Host "KEY_SECRET is not set!"
26-
} else {
27-
Write-Host "KEY_SECRET is set."
28-
}
29-
3021
- name: Setup .NET
3122
uses: actions/setup-dotnet@v4
3223
with:
@@ -53,29 +44,6 @@ jobs:
5344
exit 1
5445
}
5546
56-
- name: Decrypt security certificate
57-
shell: pwsh
58-
run: |
59-
if (-not $env:KEY_SECRET) {
60-
Write-Host "⚠️ KEY_SECRET is not set. Skipping certificate decryption."
61-
exit 0
62-
}
63-
64-
nuget install secure-file -ExcludeVersion
65-
secure-file/tools/secure-file -decrypt MetaMorpheus/smith_MM_certificate.pfx.enc -secret "$env:KEY_SECRET"
66-
67-
# This currently has issues and leads to Microsoft waringing the user that MetaMorpheus is untrusted.
68-
- name: Sign installer
69-
shell: pwsh
70-
run: |
71-
if (-not $env:KEY_SECRET -or -not $env:KEY_CERT) {
72-
Write-Host "⚠️ Certificate secrets not available. Skipping signing."
73-
exit 0
74-
}
75-
76-
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("MetaMorpheus/smith_MM_certificate.pfx", "$env:KEY_CERT", "DefaultKeySet")
77-
Set-AuthenticodeSignature -FilePath "./MetaMorpheus/MetaMorpheusSetup/bin/Release/MetaMorpheusInstaller.msi" -Certificate $cert
78-
7947
- name: Zip command-line version
8048
run: |
8149
7z a MetaMorpheus_CommandLine.zip .\MetaMorpheus\CMD\bin\Release\$env:DOTNET_FRAMEWORK\* "-x!*.xml"

MetaMorpheus/EngineLayer/FdrAnalysis/PEPAnalysisEngine.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ public void BuildFileSpecificDictionaries(List<SpectralMatch> trainingData, stri
195195
FileSpecificTimeDependantHydrophobicityAverageAndDeviation_modified = ComputeHydrophobicityValues(trainingData, true);
196196
FileSpecificTimeDependantHydrophobicityAverageAndDeviation_CZE = ComputeMobilityValues(trainingData);
197197
}
198+
if (trainingVariables.Contains("ChimeraCount"))
199+
{
200+
chimeraCountDictionary = trainingData.GroupBy(p => p.ChimeraIdString).ToDictionary(g => g.Key, g => g.Count());
201+
}
198202
}
199203

200204
public static List<int>[] GetPeptideGroupIndices(List<SpectralMatchGroup> peptides, int numGroups)

MetaMorpheus/EngineLayer/SpectralMatch.cs

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -531,21 +531,13 @@ public static int GetCountComplementaryIons(List<MatchedFragmentIon> matchedFrag
531531
{
532532
if (matchedFragments != null && matchedFragments.Count != 0)
533533
{
534-
List<int> nIons = matchedFragments.Where(f => f.NeutralTheoreticalProduct.Terminus is FragmentationTerminus.N or FragmentationTerminus.FivePrime).Select(f => f.NeutralTheoreticalProduct.FragmentNumber).ToList();
535-
List<int> cIons = matchedFragments.Where(f => f.NeutralTheoreticalProduct.Terminus is FragmentationTerminus.C or FragmentationTerminus.ThreePrime).Select(f => (peptide.BaseSequence.Length - f.NeutralTheoreticalProduct.FragmentNumber)).ToList();
536-
if (nIons.Any() && cIons.Any())
537-
{
538-
return nIons.Intersect(cIons).Count();
539-
}
540-
else
541-
{
542-
return 0;
543-
}
544-
}
545-
else
546-
{
547-
return 0;
534+
var nIons = matchedFragments.Where(f => f.NeutralTheoreticalProduct.Terminus is FragmentationTerminus.N or FragmentationTerminus.FivePrime).Select(f => f.NeutralTheoreticalProduct.FragmentNumber);
535+
var cIons = matchedFragments.Where(f => f.NeutralTheoreticalProduct.Terminus is FragmentationTerminus.C or FragmentationTerminus.ThreePrime).Select(f => peptide.BaseSequence.Length - f.NeutralTheoreticalProduct.FragmentNumber);
536+
537+
return nIons.Intersect(cIons).Count();
548538
}
539+
540+
return 0;
549541
}
550542

551543
/// <summary>
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
5+
namespace EngineLayer.Util
6+
{
7+
public static class PathSafety
8+
{
9+
// Conservative defaults for broad Windows tooling compatibility.
10+
// Increase if your environment supports long paths (\\?\).
11+
public const int DefaultMaxPath = 260;
12+
public const int DefaultMaxFileName = 255;
13+
14+
/// <summary>
15+
/// Validates and sanitizes a path for an output file. Ensures the result:
16+
/// - Ends with <paramref name="requiredEnding"/> (case-insensitive; appended if missing). The ending may be a complex suffix,
17+
/// e.g., "-{identifier}.ext", not just a simple extension.
18+
/// - Replaces invalid filename characters in the base portion with '_', preserving the required ending verbatim.
19+
/// - Avoids Windows reserved device names by prefixing an underscore (applied to the name-without-extension).
20+
/// - Trims only the base portion (before the required ending) to respect filename and total path limits.
21+
/// Throws if the directory portion alone exceeds <paramref name="maxPath"/> or if a valid filename cannot be constructed.
22+
/// </summary>
23+
/// <param name="pathToValidate">Proposed output path (relative or absolute).</param>
24+
/// <param name="requiredEnding">
25+
/// Required trailing suffix to enforce (verbatim). Examples: ".mzML", "-run42.mzML", "_v1.json".
26+
/// Matching is case-insensitive, but the supplied suffix text is preserved on output.
27+
/// </param>
28+
/// <param name="maxPath">Maximum total path length (default 260).</param>
29+
/// <param name="maxFileName">Maximum filename length (default 255).</param>
30+
/// <returns>Sanitized, safe path.</returns>
31+
/// <exception cref="ArgumentException">Null/empty inputs.</exception>
32+
/// <exception cref="PathTooLongException">Directory alone exceeds limit, or no valid filename can be constructed within limits.</exception>
33+
public static string MakeSafeOutputPath(
34+
string pathToValidate,
35+
string requiredEnding,
36+
int maxPath = DefaultMaxPath,
37+
int maxFileName = DefaultMaxFileName)
38+
{
39+
if (string.IsNullOrWhiteSpace(pathToValidate))
40+
throw new ArgumentException("Path is null or whitespace.", nameof(pathToValidate));
41+
if (string.IsNullOrWhiteSpace(requiredEnding))
42+
throw new ArgumentException("Required ending is null or whitespace.", nameof(requiredEnding));
43+
44+
// Split directory and filename
45+
string directory = Path.GetDirectoryName(pathToValidate) ?? string.Empty;
46+
string fileName = Path.GetFileName(pathToValidate);
47+
48+
// If no filename provided, synthesize a base
49+
if (string.IsNullOrWhiteSpace(fileName))
50+
fileName = "output";
51+
52+
// Ensure the filename ends with the requiredEnding (case-insensitive), without duplicating.
53+
// We will preserve the caller's requiredEnding verbatim.
54+
bool hasEnding = fileName.EndsWith(requiredEnding, StringComparison.OrdinalIgnoreCase);
55+
string basePart;
56+
if (hasEnding)
57+
{
58+
// Split into base + ending using the requiredEnding length from the end.
59+
basePart = fileName.Substring(0, fileName.Length - requiredEnding.Length);
60+
}
61+
else
62+
{
63+
basePart = fileName;
64+
}
65+
66+
// Sanitize only the base portion (so we keep the requiredEnding exactly as provided).
67+
var invalid = Path.GetInvalidFileNameChars();
68+
basePart = new string(basePart.Select(c => invalid.Contains(c) ? '_' : c).ToArray());
69+
70+
// Reassemble filename, appending ending only if it wasn't already present.
71+
string endingPart = requiredEnding;
72+
string combinedFileName = basePart + endingPart;
73+
74+
// Avoid reserved device names (Windows) by checking the name without extension
75+
combinedFileName = AvoidReservedDeviceNames(combinedFileName);
76+
77+
// Enforce filename length by trimming base (not the required ending)
78+
if (combinedFileName.Length > maxFileName)
79+
{
80+
int maxBaseLen = Math.Max(1, maxFileName - endingPart.Length);
81+
// Recompute basePart against the current combinedFileName to be safe:
82+
string currentBase = combinedFileName.Substring(0, combinedFileName.Length - endingPart.Length);
83+
if (currentBase.Length > maxBaseLen)
84+
currentBase = currentBase.Substring(0, maxBaseLen);
85+
86+
combinedFileName = currentBase + endingPart;
87+
}
88+
89+
// Combine with directory
90+
string result = CombineDirectoryAndFile(directory, combinedFileName);
91+
92+
// Enforce overall path limit by trimming base further if necessary
93+
if (result.Length > maxPath)
94+
{
95+
string dirWithSep = EnsureDirWithSeparator(directory);
96+
int maxFileLenGivenDir = maxPath - dirWithSep.Length;
97+
if (maxFileLenGivenDir <= 0)
98+
throw new PathTooLongException("Directory portion exceeds maximum path length limit.");
99+
100+
int allowedBase = Math.Max(1, maxFileLenGivenDir - endingPart.Length);
101+
if (allowedBase <= 0)
102+
throw new PathTooLongException("Cannot construct a valid filename within the path length limit.");
103+
104+
// Trim base accordingly
105+
string trimmedBase = combinedFileName.Substring(0, Math.Max(1, allowedBase));
106+
if (trimmedBase.Length > allowedBase)
107+
trimmedBase = trimmedBase.Substring(0, allowedBase);
108+
109+
combinedFileName = trimmedBase + endingPart;
110+
result = dirWithSep + combinedFileName;
111+
112+
if (result.Length > maxPath)
113+
throw new PathTooLongException("Resulting path exceeds maximum path length after trimming.");
114+
}
115+
116+
return result;
117+
}
118+
119+
private static string EnsureDirWithSeparator(string directory)
120+
{
121+
if (string.IsNullOrEmpty(directory))
122+
return string.Empty;
123+
char sep = Path.DirectorySeparatorChar;
124+
return directory.EndsWith(sep.ToString(), StringComparison.Ordinal) ? directory : directory + sep;
125+
}
126+
127+
private static string CombineDirectoryAndFile(string directory, string fileName)
128+
{
129+
if (string.IsNullOrEmpty(directory))
130+
return fileName;
131+
return Path.Combine(directory, fileName);
132+
}
133+
134+
private static string AvoidReservedDeviceNames(string fileName)
135+
{
136+
string nameNoExt = Path.GetFileNameWithoutExtension(fileName);
137+
string ext = Path.GetExtension(fileName);
138+
139+
var reserved = new[]
140+
{
141+
"con", "prn", "aux", "nul",
142+
"com1","com2","com3","com4","com5","com6","com7","com8","com9",
143+
"lpt1","lpt2","lpt3","lpt4","lpt5","lpt6","lpt7","lpt8","lpt9"
144+
};
145+
146+
if (reserved.Contains(nameNoExt.ToLowerInvariant()))
147+
nameNoExt = "_" + nameNoExt;
148+
149+
return nameNoExt + ext;
150+
}
151+
}
152+
}

0 commit comments

Comments
 (0)