Skip to content

Commit 7b4d72a

Browse files
Embed and verify PE version info across built binaries and CI (#8)
## Summary - **`jiffshot.exe` (`assets/icon_res.rs`, `app/build.rs`, `app/src/main.rs`)**: Emit an `RT_VERSION` (`VS_VERSION_INFO`) resource alongside the icon resources so `jiffshot.exe` has proper `FileVersion` (`X.Y.Z.0`), `ProductVersion` (`X.Y.Z`), `CompanyName` (`Devolutions Inc.`), `ProductName` (`Devolutions Jiffshot`), `FileDescription`, `InternalName`, `OriginalFilename`, and `LegalCopyright` metadata (and populates the MSI `File` table's `Version` column). Honors `JIFFSHOT_VERSION` when set and falls back to `CARGO_PKG_VERSION`. - **`Jiffshot.Package.exe` (`package/Jiffshot.Package.csproj`, `package/Program.cs`)**: Set `<Version>`, `<Company>`, `<Product>`, `<Description>`, and `<Copyright>` on the MSI packager project and verify that `jiffshot.exe`'s `FileVersionInfo` matches the target MSI version before building the installer. - **CI & Release workflows (`.github/workflows/ci.yml`, `.github/workflows/release.yml`)**: Pass `version` from `release.yml` into `ci.yml`, propagate `JIFFSHOT_VERSION` and `-p:Version`, and validate `FileVersionInfo` on `jiffshot.exe`, `rustolonia_host.dll`, `Jiffshot.Package.exe`, and the MSI-installed `jiffshot.exe`. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 5cdb61c commit 7b4d72a

7 files changed

Lines changed: 274 additions & 17 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ on:
77
workflow_dispatch:
88
workflow_call:
99
inputs:
10+
version:
11+
description: Version to stamp and verify on built binaries; defaults to Cargo.toml
12+
type: string
13+
default: ''
1014
signing_environment:
1115
description: GitHub environment for signing; empty produces unsigned CI artifacts
1216
type: string
@@ -67,20 +71,71 @@ jobs:
6771
restore-keys: ${{ matrix.rid }}-
6872

6973
- name: Build
74+
env:
75+
JIFFSHOT_VERSION: ${{ inputs.version }}
7076
run: cargo build --release --locked -p jiffshot --target ${{ matrix.target }}
7177

7278
- name: Test
7379
if: matrix.test
80+
env:
81+
JIFFSHOT_VERSION: ${{ inputs.version }}
7482
run: cargo test --release --locked --workspace --target ${{ matrix.target }}
7583

7684
- name: Package
7785
shell: pwsh
86+
env:
87+
EXPECTED_VERSION: ${{ inputs.version }}
7888
run: |
89+
$ErrorActionPreference = 'Stop'
7990
# The build script puts the Rustolonia host next to jiffshot.exe.
8091
$out = "target/${{ matrix.target }}/release"
8192
$dir = "jiffshot-${{ matrix.rid }}"
8293
New-Item -ItemType Directory -Path $dir | Out-Null
8394
Copy-Item "$out/jiffshot.exe", "$out/*.dll", "app/README-portable.txt" -Destination $dir
95+
$cargoToml = Get-Content -LiteralPath Cargo.toml -Raw
96+
if ($cargoToml -notmatch '(?ms)^\[workspace\.package\].*?^\s*version\s*=\s*"(?<v>[0-9]+\.[0-9]+\.[0-9]+)"') {
97+
throw 'Could not read [workspace.package] version from Cargo.toml'
98+
}
99+
$workspaceVersion = $Matches['v']
100+
$expectedVersion = if ([string]::IsNullOrWhiteSpace($env:EXPECTED_VERSION)) {
101+
$workspaceVersion
102+
} elseif ($env:EXPECTED_VERSION -ne $workspaceVersion) {
103+
throw "Input version '$env:EXPECTED_VERSION' does not match Cargo.toml '$workspaceVersion'"
104+
} else {
105+
$env:EXPECTED_VERSION
106+
}
107+
$expectedFileVersion = "$expectedVersion.0"
108+
$exeInfo = [System.Diagnostics.FileVersionInfo]::GetVersionInfo((Resolve-Path "$dir/jiffshot.exe"))
109+
if ($exeInfo.CompanyName -ne 'Devolutions Inc.') {
110+
throw "jiffshot.exe CompanyName '$($exeInfo.CompanyName)' does not match 'Devolutions Inc.'"
111+
}
112+
if ($exeInfo.ProductName -ne 'Devolutions Jiffshot') {
113+
throw "jiffshot.exe ProductName '$($exeInfo.ProductName)' does not match 'Devolutions Jiffshot'"
114+
}
115+
if ([string]::IsNullOrWhiteSpace($exeInfo.FileDescription)) {
116+
throw 'jiffshot.exe is missing FileDescription'
117+
}
118+
if ($exeInfo.OriginalFilename -ne 'jiffshot.exe') {
119+
throw "jiffshot.exe OriginalFilename '$($exeInfo.OriginalFilename)' does not match 'jiffshot.exe'"
120+
}
121+
if ($exeInfo.FileVersion -ne $expectedFileVersion) {
122+
throw "jiffshot.exe FileVersion '$($exeInfo.FileVersion)' does not match '$expectedFileVersion'"
123+
}
124+
if ($exeInfo.ProductVersion -ne $expectedVersion) {
125+
throw "jiffshot.exe ProductVersion '$($exeInfo.ProductVersion)' does not match '$expectedVersion'"
126+
}
127+
if ($cargoToml -notmatch 'rustolonia\s*=\s*\{[^}]*tag\s*=\s*"v(?<rv>[0-9]+\.[0-9]+\.[0-9]+)"') {
128+
throw 'Could not read rustolonia tag from Cargo.toml'
129+
}
130+
$rustoloniaVersion = $Matches['rv']
131+
$hostInfo = [System.Diagnostics.FileVersionInfo]::GetVersionInfo((Resolve-Path "$dir/rustolonia_host.dll"))
132+
if ($hostInfo.FileVersion -ne "$rustoloniaVersion.0") {
133+
throw "rustolonia_host.dll FileVersion '$($hostInfo.FileVersion)' does not match '$rustoloniaVersion.0'"
134+
}
135+
if (-not $hostInfo.ProductVersion.StartsWith($rustoloniaVersion)) {
136+
throw "rustolonia_host.dll ProductVersion '$($hostInfo.ProductVersion)' does not start with '$rustoloniaVersion'"
137+
}
138+
Write-Host "Version OK: jiffshot.exe FileVersion=$($exeInfo.FileVersion) ProductVersion=$($exeInfo.ProductVersion); rustolonia_host.dll FileVersion=$($hostInfo.FileVersion)"
84139
Get-ChildItem $dir | Format-Table Name, Length
85140
86141
- name: Sign portable binaries
@@ -143,10 +198,38 @@ jobs:
143198
144199
- name: Build the MSI
145200
shell: pwsh
201+
env:
202+
EXPECTED_VERSION: ${{ inputs.version }}
146203
run: |
147-
dotnet build package -c Release
204+
$ErrorActionPreference = 'Stop'
205+
$cargoToml = Get-Content -LiteralPath Cargo.toml -Raw
206+
if ($cargoToml -notmatch '(?ms)^\[workspace\.package\].*?^\s*version\s*=\s*"(?<v>[0-9]+\.[0-9]+\.[0-9]+)"') {
207+
throw 'Could not read [workspace.package] version from Cargo.toml'
208+
}
209+
$expectedVersion = if ([string]::IsNullOrWhiteSpace($env:EXPECTED_VERSION)) {
210+
$Matches['v']
211+
} elseif ($env:EXPECTED_VERSION -ne $Matches['v']) {
212+
throw "Input version '$env:EXPECTED_VERSION' does not match Cargo.toml '$($Matches['v'])'"
213+
} else {
214+
$env:EXPECTED_VERSION
215+
}
216+
dotnet build package -c Release -p:Version=$expectedVersion
148217
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
149-
package/bin/Release/net472/Jiffshot.Package.exe -bin bin -platform ${{ matrix.platform }} -out msi
218+
$pkgExe = Resolve-Path 'package/bin/Release/net472/Jiffshot.Package.exe'
219+
$pkgInfo = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($pkgExe)
220+
if ($pkgInfo.CompanyName -ne 'Devolutions Inc.') {
221+
throw "Jiffshot.Package.exe CompanyName '$($pkgInfo.CompanyName)' does not match 'Devolutions Inc.'"
222+
}
223+
if ($pkgInfo.ProductName -ne 'Devolutions Jiffshot') {
224+
throw "Jiffshot.Package.exe ProductName '$($pkgInfo.ProductName)' does not match 'Devolutions Jiffshot'"
225+
}
226+
if ($pkgInfo.FileVersion -ne "$expectedVersion.0") {
227+
throw "Jiffshot.Package.exe FileVersion '$($pkgInfo.FileVersion)' does not match '$expectedVersion.0'"
228+
}
229+
if (-not $pkgInfo.ProductVersion.StartsWith($expectedVersion)) {
230+
throw "Jiffshot.Package.exe ProductVersion '$($pkgInfo.ProductVersion)' does not start with '$expectedVersion'"
231+
}
232+
& $pkgExe -bin bin -platform ${{ matrix.platform }} -version $expectedVersion -out msi
150233
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
151234
Get-ChildItem msi | Format-Table Name, Length
152235
@@ -182,6 +265,10 @@ jobs:
182265
Where-Object { $_.DisplayName -eq 'Devolutions Jiffshot' })
183266
if ($product.Count -ne 1) { throw 'Expected one Devolutions Jiffshot entry in Programs and Features' }
184267
if ($product[0].Publisher -ne 'Devolutions Inc.') { throw "Expected Publisher 'Devolutions Inc.', got '$($product[0].Publisher)'" }
268+
$installedVer = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($exe)
269+
if ($installedVer.ProductVersion -ne $product[0].DisplayVersion -or $installedVer.FileVersion -ne "$($product[0].DisplayVersion).0") {
270+
throw "Installed jiffshot.exe version ($($installedVer.FileVersion) / $($installedVer.ProductVersion)) does not match MSI DisplayVersion '$($product[0].DisplayVersion)'"
271+
}
185272
$open = (Get-ItemProperty "HKLM:\Software\Classes\jiffshot.Document\shell\open\command").'(default)'
186273
if ($open -notlike "*jiffshot.exe*") { throw "the .jsd association is wrong: $open" }
187274
Get-ChildItem "$env:ProgramFiles\Devolutions\jiffshot" | Format-Table Name, Length

‎.github/workflows/release.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ jobs:
9090
id-token: write
9191
uses: ./.github/workflows/ci.yml
9292
with:
93+
version: ${{ needs.preflight.outputs.version }}
9394
signing_environment: ${{ needs.preflight.outputs.signing_environment }}
9495
secrets: inherit
9596

@@ -196,6 +197,10 @@ jobs:
196197
throw "Invalid release signature or timestamp for ${path}: $($signature.StatusMessage)"
197198
}
198199
}
200+
$exeInfo = [System.Diagnostics.FileVersionInfo]::GetVersionInfo((Resolve-Path "$portable\jiffshot.exe"))
201+
if ($exeInfo.ProductVersion -ne $env:RELEASE_VERSION -or $exeInfo.FileVersion -ne "$env:RELEASE_VERSION.0") {
202+
throw "Release jiffshot.exe ($arch) version ($($exeInfo.FileVersion) / $($exeInfo.ProductVersion)) does not match $env:RELEASE_VERSION"
203+
}
199204
Copy-Item -LiteralPath $msi -Destination "release\$msiName"
200205
Compress-Archive -Path "$portable\*" `
201206
-DestinationPath "release\Devolutions.Jiffshot-$arch-$env:RELEASE_VERSION-portable.zip"

‎app/build.rs‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
//! Embeds the program icon (see assets/icon_res.rs) and puts Rustolonia's
2-
//! host next to `jiffshot.exe`, so the build output folder runs as it is and
3-
//! can be zipped as the portable app.
1+
//! Embeds the program icon and version resource (see assets/icon_res.rs) and
2+
//! puts Rustolonia's host next to `jiffshot.exe`, so the build output folder
3+
//! runs as it is and can be zipped as the portable app.
44
55
include!("../assets/icon_res.rs");
66

77
use std::path::PathBuf;
88

99
fn main() {
10+
println!("cargo:rerun-if-changed=../assets/icon_res.rs");
1011
embed_icon("../assets/jiffshot.ico");
1112
copy_host();
1213
}

‎app/src/main.rs‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,3 +540,48 @@ fn main() -> rustolonia::Result<()> {
540540
ui::detach_all();
541541
result
542542
}
543+
544+
#[cfg(test)]
545+
#[allow(dead_code)]
546+
mod resource_tests {
547+
include!("../../assets/icon_res.rs");
548+
549+
#[test]
550+
fn pe_version_parts_parses_semver_and_strips_suffixes() {
551+
assert_eq!(pe_version_parts("0.1.0"), [0, 1, 0, 0]);
552+
assert_eq!(pe_version_parts("1.2.3.4"), [1, 2, 3, 4]);
553+
assert_eq!(pe_version_parts("2.5.14-rc.1+build.9"), [2, 5, 14, 0]);
554+
}
555+
556+
#[test]
557+
fn version_info_embeds_fixed_info_and_strings() {
558+
let block = version_info("1.2.3");
559+
let total_len = u16::from_le_bytes([block[0], block[1]]) as usize;
560+
assert_eq!(total_len, block.len());
561+
assert_eq!(u16::from_le_bytes([block[2], block[3]]), 52);
562+
assert_eq!(u16::from_le_bytes([block[4], block[5]]), 0);
563+
// VS_FIXEDFILEINFO starts at offset 40 (6-byte header + "VS_VERSION_INFO\0" in UTF-16 + 2-byte pad).
564+
assert_eq!(u32::from_le_bytes(block[40..44].try_into().unwrap()), 0xFEEF_04BD);
565+
assert_eq!(u32::from_le_bytes(block[48..52].try_into().unwrap()), 0x0001_0002);
566+
assert_eq!(u32::from_le_bytes(block[52..56].try_into().unwrap()), 0x0003_0000);
567+
assert_eq!(u32::from_le_bytes(block[56..60].try_into().unwrap()), 0x0001_0002);
568+
assert_eq!(u32::from_le_bytes(block[60..64].try_into().unwrap()), 0x0003_0000);
569+
let utf16: Vec<u16> = block
570+
.as_chunks::<2>()
571+
.0
572+
.iter()
573+
.map(|&b| u16::from_le_bytes(b))
574+
.collect();
575+
let decoded = String::from_utf16_lossy(&utf16);
576+
for expected in [
577+
"Devolutions Inc.",
578+
"Devolutions Jiffshot",
579+
"jiffshot.exe",
580+
"1.2.3.0",
581+
"1.2.3",
582+
] {
583+
assert!(decoded.contains(expected), "missing {expected:?} in VS_VERSION_INFO");
584+
}
585+
}
586+
}
587+

‎assets/icon_res.rs‎

Lines changed: 120 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
// Included by the app's build script: turns `assets/jiffshot.ico` into a
2-
// compiled resource file holding the program icon, and hands it to the
3-
// linker. MSVC's linker takes `.res` files directly, so no resource compiler
4-
// (or Windows SDK) is needed.
1+
// Included by the app's build script: turns `assets/jiffshot.ico` and the
2+
// package metadata into a compiled resource file holding the program icon and
3+
// `VS_VERSION_INFO`, and hands it to the linker. MSVC's linker takes `.res`
4+
// files directly, so no resource compiler (or Windows SDK) is needed.
55

66
use std::path::Path;
77

8-
/// Writes `ico` as a `.res` file: one RT_ICON per image, then the
9-
/// RT_GROUP_ICON (id 1) that lists them, which is the icon Windows shows
10-
/// for the executable.
11-
fn write_icon_res(ico: &[u8], out: &Path) -> std::io::Result<()> {
8+
/// Writes `ico` and `VS_VERSION_INFO` for `version` as a `.res` file: one
9+
/// `RT_ICON` per image, the `RT_GROUP_ICON` (id 1) that lists them, and
10+
/// `RT_VERSION` (id 1).
11+
fn write_icon_res(ico: &[u8], version: &str, out: &Path) -> std::io::Result<()> {
1212
let u16_at = |i: usize| u16::from_le_bytes([ico[i], ico[i + 1]]);
1313
let u32_at = |i: usize| u32::from_le_bytes([ico[i], ico[i + 1], ico[i + 2], ico[i + 3]]);
1414
let bad = || std::io::Error::new(std::io::ErrorKind::InvalidData, "not an .ico file");
@@ -39,9 +39,106 @@ fn write_icon_res(ico: &[u8], out: &Path) -> std::io::Result<()> {
3939
}
4040
// RT_GROUP_ICON.
4141
entry(&mut res, 14, 1, 0x1030, &group);
42+
// RT_VERSION.
43+
entry(&mut res, 16, 1, 0x0030, &version_info(version));
4244
std::fs::write(out, res)
4345
}
4446

47+
/// Builds a `VS_VERSION_INFO` block for `jiffshot.exe` at `version`.
48+
fn version_info(version: &str) -> Vec<u8> {
49+
let parts = pe_version_parts(version);
50+
let file_version = format!("{}.{}.{}.{}", parts[0], parts[1], parts[2], parts[3]);
51+
let mut fixed = Vec::with_capacity(52);
52+
let ver_ms = (u32::from(parts[0]) << 16) | u32::from(parts[1]);
53+
let ver_ls = (u32::from(parts[2]) << 16) | u32::from(parts[3]);
54+
for word in [
55+
0xFEEF_04BDu32, // dwSignature
56+
0x0001_0000, // dwStrucVersion (1.0)
57+
ver_ms, // dwFileVersionMS
58+
ver_ls, // dwFileVersionLS
59+
ver_ms, // dwProductVersionMS
60+
ver_ls, // dwProductVersionLS
61+
0x0000_003F, // dwFileFlagsMask (VS_FFI_FILEFLAGSMASK)
62+
0, // dwFileFlags
63+
0x0004_0004, // dwFileOS (VOS_NT_WINDOWS32)
64+
0x0000_0001, // dwFileType (VFT_APP)
65+
0, // dwFileSubtype
66+
0, // dwFileDateMS
67+
0, // dwFileDateLS
68+
] {
69+
fixed.extend_from_slice(&word.to_le_bytes());
70+
}
71+
72+
let strings = [
73+
("CompanyName", "Devolutions Inc."),
74+
("FileDescription", env!("CARGO_PKG_DESCRIPTION")),
75+
("FileVersion", file_version.as_str()),
76+
("InternalName", "jiffshot"),
77+
("LegalCopyright", "Copyright (c) Devolutions Inc."),
78+
("OriginalFilename", "jiffshot.exe"),
79+
("ProductName", "Devolutions Jiffshot"),
80+
("ProductVersion", version),
81+
];
82+
let mut string_entries = Vec::with_capacity(strings.len());
83+
for (key, val) in strings {
84+
let mut value_bytes = Vec::new();
85+
for u in val.encode_utf16().chain(std::iter::once(0)) {
86+
value_bytes.extend_from_slice(&u.to_le_bytes());
87+
}
88+
let value_words = (value_bytes.len() / 2) as u16;
89+
string_entries.push(ver_node(key, 1, value_words, &value_bytes, &[]));
90+
}
91+
let string_table = ver_node("040904b0", 1, 0, &[], &string_entries);
92+
let string_file_info = ver_node("StringFileInfo", 1, 0, &[], &[string_table]);
93+
// US English (0x0409), Unicode (1200 = 0x04B0).
94+
let translation = ver_node("Translation", 0, 4, &[0x09, 0x04, 0xB0, 0x04], &[]);
95+
let var_file_info = ver_node("VarFileInfo", 1, 0, &[], &[translation]);
96+
ver_node(
97+
"VS_VERSION_INFO",
98+
0,
99+
fixed.len() as u16,
100+
&fixed,
101+
&[string_file_info, var_file_info],
102+
)
103+
}
104+
105+
/// Parses up to four `u16` version components from `version` (stripping any
106+
/// `-prerelease` or `+build` suffix).
107+
fn pe_version_parts(version: &str) -> [u16; 4] {
108+
let numeric = version.split(['-', '+']).next().unwrap_or(version);
109+
let mut parts = [0u16; 4];
110+
for (slot, part) in parts.iter_mut().zip(numeric.split('.')) {
111+
if let Ok(value) = part.parse::<u16>() {
112+
*slot = value;
113+
}
114+
}
115+
parts
116+
}
117+
118+
/// One node in a `VS_VERSION_INFO` tree: `(wLength, wValueLength, wType,
119+
/// szKey, padding, Value, padding, Children)`.
120+
fn ver_node(key: &str, kind: u16, value_len: u16, value: &[u8], children: &[Vec<u8>]) -> Vec<u8> {
121+
let mut node = vec![0u8; 6];
122+
for u in key.encode_utf16().chain(std::iter::once(0)) {
123+
node.extend_from_slice(&u.to_le_bytes());
124+
}
125+
while !node.len().is_multiple_of(4) {
126+
node.push(0);
127+
}
128+
node.extend_from_slice(value);
129+
for child in children {
130+
while !node.len().is_multiple_of(4) {
131+
node.push(0);
132+
}
133+
node.extend_from_slice(child);
134+
}
135+
let len = node.len() as u16;
136+
node[0..2].copy_from_slice(&len.to_le_bytes());
137+
node[2..4].copy_from_slice(&value_len.to_le_bytes());
138+
node[4..6].copy_from_slice(&kind.to_le_bytes());
139+
node
140+
}
141+
45142
/// One resource with a numeric type and name, in US English, padded to a
46143
/// 4-byte boundary.
47144
fn entry(res: &mut Vec<u8>, kind: u16, name: u16, flags: u16, data: &[u8]) {
@@ -56,19 +153,30 @@ fn entry(res: &mut Vec<u8>, kind: u16, name: u16, flags: u16, data: &[u8]) {
56153
res.extend_from_slice(&0u32.to_le_bytes());
57154
res.extend_from_slice(&0u32.to_le_bytes());
58155
res.extend_from_slice(data);
59-
while res.len() % 4 != 0 {
156+
while !res.len().is_multiple_of(4) {
60157
res.push(0);
61158
}
62159
}
63160

64-
/// Embeds `ico` (a path relative to the crate) as the executable's icon.
161+
/// Embeds `ico` (a path relative to the crate) and `VS_VERSION_INFO` into the
162+
/// executable.
65163
fn embed_icon(ico: &str) {
66164
println!("cargo:rerun-if-changed={ico}");
67-
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") || std::env::var("CARGO_CFG_TARGET_ENV").as_deref() != Ok("msvc") {
165+
println!("cargo:rerun-if-env-changed=JIFFSHOT_VERSION");
166+
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows")
167+
|| std::env::var("CARGO_CFG_TARGET_ENV").as_deref() != Ok("msvc")
168+
{
68169
return;
69170
}
171+
let version = std::env::var("JIFFSHOT_VERSION")
172+
.ok()
173+
.map(|v| v.trim().to_string())
174+
.filter(|v| !v.is_empty())
175+
.or_else(|| std::env::var("CARGO_PKG_VERSION").ok())
176+
.unwrap_or_else(|| "0.0.0".to_string());
70177
let bytes = std::fs::read(ico).unwrap_or_else(|e| panic!("reading {ico}: {e}"));
71178
let out = Path::new(&std::env::var("OUT_DIR").expect("cargo sets OUT_DIR")).join("icon.res");
72-
write_icon_res(&bytes, &out).unwrap_or_else(|e| panic!("writing {}: {e}", out.display()));
179+
write_icon_res(&bytes, &version, &out)
180+
.unwrap_or_else(|e| panic!("writing {}: {e}", out.display()));
73181
println!("cargo:rustc-link-arg-bins={}", out.display());
74182
}

‎package/Jiffshot.Package.csproj‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
<LangVersion>latest</LangVersion>
1212
<Nullable>enable</Nullable>
1313
<RootNamespace>Jiffshot.Package</RootNamespace>
14+
<Version Condition="'$(Version)' == ''">0.1.0</Version>
15+
<Company>Devolutions Inc.</Company>
16+
<Product>Devolutions Jiffshot</Product>
17+
<Description>Jiffshot MSI packager</Description>
18+
<Copyright>Copyright (c) Devolutions Inc.</Copyright>
1419
</PropertyGroup>
1520

1621
<ItemGroup>

0 commit comments

Comments
 (0)