Skip to content

Commit 875481c

Browse files
committed
fix: standalone EXE bundling, uninstall kills process, dedup by hash; two-artifact release with compressed standalone and lightweight installer
1 parent 9614ff6 commit 875481c

File tree

5 files changed

+130
-17
lines changed

5 files changed

+130
-17
lines changed

.github/workflows/release.yml

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,23 @@ jobs:
2929
- name: Run tests
3030
run: dotnet test --configuration Release --no-restore
3131

32-
- name: Publish self-contained EXE
32+
# ── Standalone EXE: self-contained, compressed, all native libs bundled ──
33+
- name: Publish standalone EXE
3334
run: >
3435
dotnet publish src/ClipHive/ClipHive.csproj
3536
-c Release -r win-x64 --self-contained true
3637
-p:PublishSingleFile=true
37-
-p:EnableCompressionInSingleFile=false
38-
-p:PublishReadyToRun=true
39-
-p:IncludeNativeLibrariesForSelfExtract=false
38+
-p:EnableCompressionInSingleFile=true
39+
-p:PublishReadyToRun=false
40+
-p:IncludeNativeLibrariesForSelfExtract=true
41+
-p:Version=${{ env.VERSION }}
42+
-o dist/standalone
43+
44+
# ── Installer payload: framework-dependent, no runtime bundled ───────────
45+
- name: Publish installer payload
46+
run: >
47+
dotnet publish src/ClipHive/ClipHive.csproj
48+
-c Release -r win-x64 --self-contained false
4049
-p:Version=${{ env.VERSION }}
4150
-o dist/release
4251
@@ -48,9 +57,9 @@ jobs:
4857
run: |
4958
"C:\Program Files (x86)\Inno Setup 6\ISCC.exe" /DMyAppVersion=${{ env.VERSION }} installer\ClipHive.iss
5059
51-
- name: Rename standalone EXE for release
60+
- name: Copy standalone EXE to dist
5261
shell: bash
53-
run: cp dist/release/ClipHive.exe "dist/ClipHive-${{ env.VERSION }}-Standalone.exe"
62+
run: cp dist/standalone/ClipHive.exe "dist/ClipHive-${{ env.VERSION }}-Standalone.exe"
5463

5564
- name: Create GitHub Release
5665
uses: softprops/action-gh-release@v2
@@ -65,12 +74,13 @@ jobs:
6574
6675
| File | Description |
6776
|------|-------------|
68-
| `ClipHive-${{ env.VERSION }}-Setup.exe` | **Recommended** — Installer with setup wizard (50 MB) |
69-
| `ClipHive-${{ env.VERSION }}-Standalone.exe` | Portable EXE, no installer needed (~163 MB, no .NET required) |
77+
| `ClipHive-${{ env.VERSION }}-Setup.exe` | **Recommended** — Installer (~20 MB). Requires [.NET 8 Desktop Runtime](https://dotnet.microsoft.com/download/dotnet/8.0) — the installer will prompt if it's missing. |
78+
| `ClipHive-${{ env.VERSION }}-Standalone.exe` | Portable EXE, no installer needed (~90 MB, .NET runtime bundled) |
7079
7180
### Requirements
72-
- Windows 10 version 1903 or later (Windows 11 fully supported)
73-
- No .NET installation required — both builds are self-contained
81+
- Windows 10 version 1809 or later (Windows 11 fully supported)
82+
- **Installer**: .NET 8 Windows Desktop Runtime (prompted if missing)
83+
- **Standalone**: No .NET installation required — runtime is bundled
7484
7585
### First Run
7686
After installation, ClipHive starts in the system tray.

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [1.3.2] - 2026-04-15
11+
12+
### Fixed
13+
- **Standalone EXE now works** — Native DLLs (SQLite, WPF) were previously excluded
14+
from the single-file bundle (`IncludeNativeLibrariesForSelfExtract=false`), causing the
15+
standalone EXE to crash on launch. All dependencies are now embedded.
16+
- **Uninstaller now quits the app first** — ClipHive is forcefully terminated via
17+
`taskkill` before file removal, ensuring a clean uninstall with no locked-file errors.
18+
- **Duplicate clipboard entries** — Copying the same content multiple times no longer
19+
adds duplicate rows. The existing entry is bumped to the top instead (timestamp
20+
updated). Uses a SHA-256 content hash per row — no decryption overhead on copy.
21+
22+
### Changed
23+
- **Two release artifacts** with different size/dependency trade-offs:
24+
- **Setup installer** (~20 MB) — framework-dependent; requires .NET 8 Windows Desktop
25+
Runtime (installer checks and opens the download page if missing).
26+
- **Standalone EXE** (~90 MB) — self-contained + compressed; no .NET required.
27+
- Standalone EXE compressed with `EnableCompressionInSingleFile=true`, dropped
28+
`PublishReadyToRun` — size reduced from ~225 MB to ~90 MB.
29+
1030
## [1.3.1] - 2026-04-15
1131

1232
### Fixed

installer/ClipHive.iss

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#define MyAppName "ClipHive"
2-
#define MyAppVersion "1.3.1"
2+
#define MyAppVersion "1.3.2"
33
#define MyAppPublisher "ClipHive Contributors"
44
#define MyAppURL "https://github.com/levitasOrg/cliphive"
55
#define MyAppExeName "ClipHive.exe"
@@ -59,6 +59,21 @@ Type: filesandordirs; Name: "{localappdata}\ClipHive"
5959

6060
[Code]
6161
62+
// ─── .NET 8 Desktop Runtime check ───────────────────────────────────────────
63+
64+
function IsDotNet8DesktopInstalled(): Boolean;
65+
var
66+
FindRec: TFindRec;
67+
begin
68+
Result := False;
69+
// Check for any 8.x.y folder under the Windows Desktop App shared runtime
70+
if FindFirst(ExpandConstant('{commonpf64}\dotnet\shared\Microsoft.WindowsDesktop.App\8.*'), FindRec) then
71+
begin
72+
Result := True;
73+
FindClose(FindRec);
74+
end;
75+
end;
76+
6277
// ─── Already-installed check ────────────────────────────────────────────────
6378
6479
function IsAlreadyInstalled(): Boolean;
@@ -93,6 +108,22 @@ var
93108
begin
94109
Result := True;
95110
111+
// ── .NET 8 Desktop Runtime prerequisite check ──────────────────────────────
112+
if not IsDotNet8DesktopInstalled() then
113+
begin
114+
Answer := MsgBox(
115+
'ClipHive requires the .NET 8 Windows Desktop Runtime, which was not found.' + #13#10 + #13#10 +
116+
'Click Yes to open the Microsoft download page, then re-run this installer.' + #13#10 +
117+
'Click No to cancel.',
118+
mbError, MB_YESNO);
119+
if Answer = IDYES then
120+
ShellExec('open',
121+
'https://dotnet.microsoft.com/download/dotnet/8.0',
122+
'', '', SW_SHOWNORMAL, ewNoWait, ResultCode);
123+
Result := False;
124+
Exit;
125+
end;
126+
96127
if not IsAlreadyInstalled() then
97128
Exit;
98129
@@ -160,10 +191,19 @@ begin
160191
Result := True;
161192
end;
162193
163-
// ─── Uninstall: remove Run registry key ────────────────────────────────────
194+
// ─── Uninstall: kill process then clean up registry ────────────────────────
164195
165196
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
197+
var
198+
ResultCode: Integer;
166199
begin
200+
if CurUninstallStep = usUninstall then
201+
begin
202+
// Forcefully quit ClipHive if it is running so files can be deleted cleanly.
203+
Exec('taskkill.exe', '/F /IM ClipHive.exe', '', SW_HIDE,
204+
ewWaitUntilTerminated, ResultCode);
205+
end;
206+
167207
if CurUninstallStep = usPostUninstall then
168208
RegDeleteValue(HKCU, 'Software\Microsoft\Windows\CurrentVersion\Run', 'ClipHive');
169209
end;

src/ClipHive/ClipHive.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
1111
<RootNamespace>ClipHive</RootNamespace>
1212
<AssemblyName>ClipHive</AssemblyName>
13-
<Version>1.3.1</Version>
14-
<FileVersion>1.3.1.0</FileVersion>
13+
<Version>1.3.2</Version>
14+
<FileVersion>1.3.2.0</FileVersion>
1515
<Copyright>Copyright © 2026 ClipHive</Copyright>
1616
<Description>Lightweight clipboard history manager for Windows</Description>
1717
<Company>ClipHive</Company>

src/ClipHive/Services/StorageService.cs

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,18 @@ content_type TEXT NOT NULL DEFAULT 'text'
8484
{
8585
// Column already exists — ignore.
8686
}
87+
88+
// Migrate existing databases that lack the content_hash column (used for deduplication).
89+
try
90+
{
91+
using var alter = _connection.CreateCommand();
92+
alter.CommandText = "ALTER TABLE clipboard_items ADD COLUMN content_hash TEXT;";
93+
alter.ExecuteNonQuery();
94+
}
95+
catch (SqliteException ex) when (ex.Message.Contains("duplicate column"))
96+
{
97+
// Column already exists — ignore.
98+
}
8799
}
88100

89101
/// <summary>Maximum non-pinned items retained. Updated when settings change.</summary>
@@ -95,28 +107,59 @@ public async Task AddAsync(string plaintext, string? sourceApp = null)
95107
ArgumentNullException.ThrowIfNull(plaintext);
96108
ThrowIfDisposed();
97109

98-
var (ciphertext, iv, tag) = _encryption.Encrypt(plaintext);
110+
// SHA-256 of the plaintext — used to detect duplicates without decrypting all rows.
111+
string hash = ComputeHash(plaintext);
99112

100113
await _lock.WaitAsync().ConfigureAwait(false);
101114
try
102115
{
116+
// If the same content already exists, bump its timestamp to the top instead
117+
// of inserting a duplicate entry.
118+
using var check = _connection.CreateCommand();
119+
check.CommandText = """
120+
SELECT id FROM clipboard_items
121+
WHERE content_hash = $hash AND content_type = 'text'
122+
LIMIT 1;
123+
""";
124+
check.Parameters.AddWithValue("$hash", hash);
125+
var existingId = check.ExecuteScalar();
126+
127+
if (existingId is not null)
128+
{
129+
using var bump = _connection.CreateCommand();
130+
bump.CommandText = "UPDATE clipboard_items SET created_at = $ca WHERE id = $id;";
131+
bump.Parameters.AddWithValue("$ca", DateTime.UtcNow.ToString("O"));
132+
bump.Parameters.AddWithValue("$id", (long)existingId);
133+
bump.ExecuteNonQuery();
134+
return;
135+
}
136+
137+
// New item — encrypt and insert.
138+
var (ciphertext, iv, tag) = _encryption.Encrypt(plaintext);
103139
using var cmd = _connection.CreateCommand();
104140
cmd.CommandText = """
105-
INSERT INTO clipboard_items (ciphertext, iv, tag, created_at, source_app, is_pinned, content_type)
106-
VALUES ($ct, $iv, $tag, $ca, $sa, 0, 'text');
141+
INSERT INTO clipboard_items (ciphertext, iv, tag, created_at, source_app, is_pinned, content_type, content_hash)
142+
VALUES ($ct, $iv, $tag, $ca, $sa, 0, 'text', $hash);
107143
""";
108144
cmd.Parameters.AddWithValue("$ct", ciphertext);
109145
cmd.Parameters.AddWithValue("$iv", iv);
110146
cmd.Parameters.AddWithValue("$tag", tag);
111147
cmd.Parameters.AddWithValue("$ca", DateTime.UtcNow.ToString("O"));
112148
cmd.Parameters.AddWithValue("$sa", sourceApp is null ? DBNull.Value : (object)sourceApp);
149+
cmd.Parameters.AddWithValue("$hash", hash);
113150
cmd.ExecuteNonQuery();
114151

115152
PurgeOverLimitUnlocked(MaxHistoryCount);
116153
}
117154
finally { _lock.Release(); }
118155
}
119156

157+
private static string ComputeHash(string plaintext)
158+
{
159+
var bytes = System.Text.Encoding.UTF8.GetBytes(plaintext);
160+
return Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(bytes));
161+
}
162+
120163
/// <inheritdoc/>
121164
public async Task AddImageAsync(byte[] imageBytes, string? sourceApp = null, string? ocrText = null)
122165
{

0 commit comments

Comments
 (0)