Files
rustdesk/.github/workflows/flutter-build.yml
T
RustDeskandClaude Opus 5.5 e9ddbd8f46 cursor: bound the decoded cursor caches on the Flutter client (#16304)
* cursor: name a shape by what it looks like, not by its handle

A cursor id was the platform's handle for the shape: HCURSOR on
Windows, the XFixes serial on X11. Apps mint a new handle for a shape
they have shown before (Chrome, Electron, Qt custom cursors), and an
X11 animated cursor gets one per frame, so one arrow arrived under
thousands of ids over a session. The controlled side cached and sent
it again under each, and the controller decoded, rendered and kept it
again under each, and grew by gigabytes over weeks of connection.

The controlled side now names a shape by an xxh3 hash of its size,
hotspot and pixels, taken once when a handle is first seen, before
the pixels are compressed. The per-connection send already sends a
shape once and afterwards only its id, so a shape now crosses once
however many handles it has. The id is opaque in the protocol, so
controllers of any version accept it. The DRM backend already named
shapes by content with FNV-1a; it uses the same hash now.

A peer before 1.5.0 still names shapes by handle, and the fix has to
work against every deployed peer. For those peers the controller
hashes each CursorData's compressed colors with its geometry: one
peer compresses the same pixels to the same bytes, so a shape seen
before is recognised without being decompressed. The UI gets that
shape once, under one id, and every later handle for it is passed on
as a cursor_id of that id. The map from the peer's ids is kept for the
connection, unbounded, since the peer sends a shape once and may
select any id it named again; an entry is two integers. From 1.5.0
the controller passes the peer's ids through unchanged, on the old
path.

The web core in flutter/web does the same in its own tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: hold a content id to what the web client can read

The web client decodes a u64 into a JS number and throws on a value
past 2^53, which drops the whole message. A full 64-bit xxh3 id is
almost always past it, so a web client connected to a peer on this
build would never have been given a cursor shape. DRM's ids were
already full width, so web clients lost the cursor on DRM peers too;
the controlled side now renames those as well.

The id keeps the hash's low 53 bits. Two of ten thousand distinct
shapes then collide with a chance of about one in two hundred million.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: hand the UI a shape's pixels as they are, not as text

A shape's pixels went to the Flutter UI as a JSON array of integers
inside the cursor_data event: the core serialised a 1 MiB shape into
up to 4 MiB of text on the receive loop, and the UI parsed that into a
million-element list and copied it into bytes, on the UI thread. The
event, text and all, was then kept for the session so that a tab
moved to another window could replay it, and a reconnect, on which the
peer sends every shape again, appended another copy of each.

The core now sends a shape as an EventToUI::Cursor variant carrying
Vec<u8>, which flutter_rust_bridge hands to Dart as a Uint8List, on
the same per-session stream as the events so that it keeps its order
with the cursor_id events around it. The web core calls onCursorData
with the Uint8Array, as it calls onRgba for a frame. The id stays
text, since the peer's ids can exceed Dart's int.

The replay keeps each shape's pixels once, by id, and encodes them as
base64 only when a tab moves. A shape arriving is also recorded as the
last one selected, since the replay goes in first-seen order; before,
a tab moved right after a new shape came back on the previous one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: let a controller ask the peer for a shape again

A peer sends each shape once per connection and afterwards only its
id, so a controller must keep every shape it was sent for as long as
it is connected. The controlled side now answers
Misc::request_cursor_data with the CursorData of that id, from the
shapes it has sent, and only to a connection the cursor service
would send it to. A controller from 1.5.0 on can then bound what it
keeps and ask again for a shape it let go. None asks yet: the peer has
to have the answer before a controller can rely on it, and peers are
updated last.

The shapes are kept by content id, and every handle for a shape
shares its one message, where each handle used to keep its own copy.
They are dropped with the service's own cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: keep a shape as its ui.Image and nothing else

Each shape the UI had been sent stayed in four copies for the session:
the pixels for a tab move, an image package copy to resize from (on a
macOS or Linux controller of a Windows or Linux peer, a second one
decoded back from a PNG), the bytes handed to the native cursor, and
the ui.Image painted when the peer moves the pointer.

A shape now keeps only its ui.Image. The resize source and the native
bytes exist while a native cursor is being registered and are dropped
once it is; a shape shown again at a raster it was registered at needs
neither. For a raster it lacks, the pixels are read back from the
ui.Image, which returns them exactly as they came. A tab move reads
every shape back the same way, and the moved window decodes them as
it would have on arrival.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: pin that a released shape keeps neither its pixels nor its bytes

The shape in use once registered, the shapes not in use, and the
pixels read back for a moved tab are all dropped; the tests now say so,
so that a later change keeping one of them fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: keep one native cursor per shape, and each tab its own

A shape got a native cursor for every raster it was shown at, and all
of them stayed until the session ended, so every zoom or DPR change
added a native cursor for each shape in use. A shape now keeps the one
at its current raster; the one it replaces is deleted the next time a
cursor already registered is built, by when its successor has been
activated. A raster returned to before that takes its cursor back
instead of registering another under the same name.

The native cursors of a window's engine are shared by its tabs, and
the predefined cursors had one name in all of them, so a tab closing
deleted the default and forbidden cursors the other tabs still showed.
Names are now scoped to the tab's cursor model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: keep the shapes compressed in the core, and only the one in use in the UI

The UI kept every shape as a ui.Image, one raw copy each, for as long
as the session lasted, because the peer sends a shape once and a
shape might have to be drawn again. The core receives the shapes
compressed, at a few hundredths of that, and now keeps them so:
Session::cursor_shapes holds each shape that decoded, as the peer sent
it, under the id the UI knows it by. The colors are copied, since the
message's may be a slice of a larger received buffer.

The UI keeps a ui.Image for the shape in use alone. A shape shown
again at a raster its native cursor has needs no pixels at all; for a
raster it lacks, for painting it, or for the window a tab moved to,
the UI asks the core through session_get_cursor_shape, which
decompresses and checks the shape again with decode_cursor_data, the
same size and decompression limits as on arrival. A shape the core
cannot give is not asked for again until the peer sends it. A tab
move now carries only the id in use, and nothing is read back from
the GPU.

The web core keeps the shapes the same way and gives them back through
getCursorShape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: a decode that finishes after the session clears keeps nothing

A shape asked of the core while a tab closed was decoded after the
cursor model had been cleared and stored in it, where nothing disposed
of its ui.Image again; a shape whose cache failed to build left its
ui.Image undisposed as well. A clear now starts a new generation, and a
fetch or decode from an earlier one disposes what it made. A shape the
core gave but that did not decode is not asked for again on every
frame it is painted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: keep a native cursor for the shape in use alone

Every shape the peer showed kept a native cursor for the session, each
a raster the size of the shape as displayed. A shape switched away
from now gives its native cursor up the way a replaced raster does:
deleted the next time a cursor already registered is built, taken back
if the shape returns first. A shape shown again after that is rebuilt
from the compressed copy the core keeps. The predefined cursors are
not the peer's shapes and stay registered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: keep the native cursors of the 16 shapes used last

Keeping a native cursor for the shape in use alone rebuilt one on
nearly every switch, since the pointer moves among a few shapes: the
arrow, text, hand, four resize arrows, wait. The native cursors of the
peer's shapes are now kept in order of use, 16 of them, and the one
used longest ago gives its cursor up the way a replaced raster does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: forget the native cursors a session clear deletes

The clear deleted every registered native cursor but kept their names,
so a cursor model used again, as the mobile client's is, took each
name for registered and showed a cursor that no longer existed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: hold a native cursor to 512 pixels a side

A native cursor could be registered up to 1024 pixels a side, four
MiB of pixels each, which with sixteen kept made 64 MiB at worst. The
largest cursor a system shows is 256 pixels (Windows' largest pointer
size), so a raster twice that is shown no larger: the cursor is drawn
smaller rather than rejected, and sixteen kept come to 16 MiB at most.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: a shape switched away from while it decodes is not marked lost

A shape asked of the core was taken for undecodable when it was no
longer kept once its decode finished, but a shape the peer switched
away from in the meantime is let go on purpose, and marking it made
every later request for it be skipped: its native cursor stayed
deferred and its painted cursor missing for the rest of the session.
The decode now reports whether it decoded, and only a failure marks
the shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* flutter: leave two untouched signatures as they were

dart format had rewrapped registerEventHandler and sessionSetCommon
next to the cursor additions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: read a shape's pixels from where its view starts

The pixels used to be copied out of the JSON event into a fresh list,
so the list's buffer began at its first pixel. Now they come straight
from the core, native or web, as whatever Uint8List it hands over, and
img2's fromBytes takes the buffer, not the list, so a view into a
larger buffer would have been read from the buffer's start. Neither
core gives a view today; this stops depending on that.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: a DRM cache reset keeps the shape it was making room for

The shape being sent was filed in CURSOR_SHAPES before the DRM ceiling
cleared it with everything else, so a controller asking for the shape
on screen got nothing until the next new shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: delete a replaced native cursor at the next build that shows one

A replaced native cursor was deleted only by a build of a cursor
already registered, so a run of new shapes, each shown once, left
every replaced cursor registered: 16 on the books, all of them in the
system. Now every build that shows a cursor deletes the ones replaced
before it, right before it registers or takes one back; what replaced
them was activated in an earlier frame, so they are off screen. A
build that has to ask for pixels shows nothing new and deletes
nothing, so a raster returned to right after being replaced still
takes its cursor back; one returned to later registers again.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: a shape that does not decode leaves no handle and no copy

CursorDedupe named a handle as it hashed the shape, before the shape
was checked, so a peer sending shapes that fail the checks under new
handles grew the map with nothing the UI could ever select, and such
a shape took over the handle of one that had decoded. The handle is
now named once its shape decoded, or was shown before.

The flutter build also copied a shape's compressed colors out of the
message before the checks, so a message with a small size and huge
colors was copied whole before being rejected. The copy is made once
the shape decoded; until then the message's own bytes are held.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: remember the shape the core lacks only while it is in use

Every id the core could not give was kept in a set for the session,
so a peer sending cursor_id after cursor_id of shapes it never sent
grew it by a string each, at a few bytes of traffic per entry. The
mark exists so that painting does not ask for the shape in use on
every frame, and the shape in use is one, so one mark is kept: the
peer selecting another shape forgets it, and selecting a lost shape
again asks once more.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: forget the shape the core lacks once the peer selects another

The mark was replaced only by another shape the core lacked, so a
lost shape selected again after a known one was still not asked for.
Selecting any other shape now clears it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: leave the DRM cursor's own id as it was

The wire id is cursor_content_id whatever the platform, set in
run_cursor over the shape's pixels, so the id the DRM reader derives
for itself only tells one hardware cursor from the next and keys the
service's cache. Which hash it uses does not reach a controller; the
change to xxh3 and the dependency it brought to scrap go.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: say the 1.5.0 gate means the release

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: say why the peer answers request_cursor_data with what it holds

No controller sends it yet, and a review asked for it to go because a
cache reset on the peer could leave a request unanswered. A shape the
peer's cache dropped is rebuilt, and indexed again, the next time it
is shown, before any connection names it by id, so the shape a
controller has just been told to show is always there to give; that
is the contract a future bounded controller relies on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: drop the tests nothing can fail

a_handle_is_named_once_its_shape_is_shown dates from when `name`
wrote the handle map. `name` is pure now, so its last assertion
cannot fail, and the others repeat two neighbours.
every_handle_the_peer_named_stays_for_the_connection fails only for
a bound added on purpose, which the struct's doc rules out. The Dart
arrival test is covered by the first test of its file.

The kept-shape test now asserts what `cursor_shape` is for, a copy
rather than a view of the received buffer, in place of the unknown
fields it drops. The content-id test moves out from between two
`use` lines.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: let the kept-shape tests run without the flutter feature

CI's cargo test builds the default features, and the two functions
under test are pure, so they compile for every test build and the
module loses its feature gate.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: keep native cursors for every frame of an animated cursor

An animated cursor is a shape per frame, 18 for the Windows busy cursor
and 23 for KDE's wait cursor. With 16 native cursors a cycle longer than
that missed on every frame: fetched from the core, decoded, registered
again and the replaced cursor deleted, about thirty times a second for
as long as the peer was busy. 64 holds two such animations and the
everyday set besides.

The tests settle in 10 ms instead of 100 ms, so the ones that fill the
limit stay quick.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: release only the shapes that can hold pixels

Selecting a cursor swept every shape of the session to release pixels,
though only two can hold any: the shape selected before, and a shape
that finished decoding after the peer moved on. Release those, so a
cursor_id costs the same however many shapes the peer has shown.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: show the native cursor limit holds two animations and the everyday set

64 is a working set, not the longest animation: KDE's wait and progress
cursors are 23 frames each, and a session that meets both plus a dozen
static shapes must keep them all, or every turn rebuilds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: let the shape in use keep its pixels for a new raster

Dropping them once the native cursor was registered meant every scale
change, zooming with the cursor zoomed or crossing to a display of
another scale, showed the system arrow while the core gave the shape
back and it decoded again. The shape in use now keeps them and makes the
new raster at once; a shape switched away from still keeps none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: never fall back to the default cursor on a switch

A switch to a shape without its image, or without a native cursor at
its raster, showed the default arrow until the core gave the shape back
and it decoded: the painted cursor on every switch back, the native one
after an eviction or while a new shape decoded.

- The shape shown before stays, image, hotspot and native cursor, until
  the one in use is ready.
- Where the cursor is painted, on mobile or with the remote cursor
  shown, the images of the 64 shapes used last are kept, like the native
  cursors, so an animation does not decode a frame per turn. Elsewhere
  only the shape in use keeps one.
- A shape that finishes decoding after the peer moved on is kept with
  them instead of dropped, so a fast animation fills them at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: name the dedupe's content-id set for what it holds

`shown` read as the shape on screen. It is the shapes decoded and given
to the UI, keyed by content id, so a shape arriving under a new handle
is looked up once instead of scanning the handle map: `decoded`,
`has_decoded`, and `record` for the call that fills both maps. The web
mirror is renamed the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: keep native cursors for the session, as before

Evicting native cursors and deleting a shape's replaced raster made
Windows leak more than master. The engine's deleteCustomCursor/windows
frees the HCURSOR with DeleteObject, which does not take a cursor, so a
delete frees nothing and each cursor made again for a raster, or for a
shape shown once more, leaks another handle. Keeping every raster of
every shape until the session is cleared creates each one once, as
master does.

Fixing the engine instead is not cheap: x64 runs our fork on Flutter
3.24, where the fix is one more patch to rebuild and carry across every
upgrade; arm64 runs the stock engine of a newer Flutter with the same
code, where it means porting the fork and publishing an arm64 engine
too, or waiting for an upstream fix to reach stable. Content ids keep
the count to the shapes the peer really shows.

The painted images, the compressed shapes in the core, and a switch
showing the cursor shown before are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: record the Windows measurement behind keeping native cursors

A measurement on Windows confirmed what the engine source implied: of
500 cursors made by CreateIconIndirect, DeleteObject freed none and
left 500 USER objects alive, while DestroyCursor and DestroyIcon freed
all 500. The comment now says so, that native cursors have no LRU for
that reason only, and that one can come back once both engines destroy
cursors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: look a native cursor up by the raster asked for

The key ended in the raster made last. A shape switched away from keeps
no pixels, so its raster stays where it was, and going back to a raster
it was shown at before looked up the wrong key: A at 1.0, A at 0.5, then
B, then A at 1.0 again missed the 32x32 cursor that was still there,
fetched A from the core, and showed B meanwhile. With native cursors
kept for every raster, the key now ends in the raster asked for; with
pixels, that is the raster made, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: let a shape keep its pixels until a native cursor holds them

A shape switched away from, or decoded after the peer moved on, let its
pixels go whether or not it had a native cursor, and a native cursor is
made only for the shape in use. A shape whose restore kept finishing
after the next switch, as an animation's frames can, was fetched and
decoded again every time it came back and never got a native cursor.

A shape without one now keeps its pixels until one is made, at most
kRecentShapes of them, the one waiting longest let go first. Mobile
only paints the cursor, so it lets them go as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* proto: keep cursor ids as strings in JS

A Windows peer before 1.5.0 names a cursor by its handle, sign-extended
above 2^53 when its top bit is set. The web client decodes u64 fields
into JS numbers, and ts-proto throws on a value a number cannot hold,
so each such cursor_data or cursor_id was dropped whole and the web
cursor stayed on the shape before. [jstype = JS_STRING] makes ts-proto
generate these two fields as strings; the wire format, the Rust code
generated and every other client are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: wait for a native cursor at the raster asked for, not any raster

A shape let its pixels go once it had a native cursor at some raster.
After a zoom, or on a display of another scale, an animation's frames
had native cursors at the old raster only; each was fetched again at
the new one, and a restore that finished after the next switch let its
pixels go at once, so the frames never got a native cursor at the new
raster and the cursor stayed on the one shown before. A lookup that
finds no native cursor at the raster asked for now counts the shape as
waiting for one, so a late restore keeps its pixels until it has one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: count a shape held again when its asked raster has a native cursor

A lookup that missed took the shape out of _nativeIds, but one that hit
did not put it back. A shape asked at a new raster, then at one it has a
native cursor for, still counted as waiting: a restore finishing after
the switch away kept its pixels in _awaitingNative instead of letting
them go. The set now says whether the raster asked last has a native
cursor, either way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* Revert "cursor: count a shape held again when its asked raster has a native cursor"

This reverts commit 7cefbfc7a.

_nativeIds is meant as "no raster of this shape is still missing its
native cursor", not "the raster asked last has one". A shape asked at a
new raster whose restore is on its way, then shown at a raster it has a
cursor for, still waits for the new one; counting it held let the late
restore's pixels go, so going back to the new raster fetched it again,
and with an animation's frames and a scale that keeps changing, the
loop 1e4388771 fixed could come back. Keeping the pixels costs at most
a waiting slot, within the 64. A test now locks this: a raster still
missing keeps a late restore whatever was shown meanwhile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: reuse a shape sent before without compressing it again

A handle not seen before sent the service through the whole path:
capture the pixels, name them by content, compress them, and only then
find the shape already in CURSOR_SHAPES and drop what it compressed. An
app that mints a new handle for the same cursor, as Chrome and Electron
do, paid a compress per handle, a few ms for an enlarged cursor. The
content id is now looked up first, and only a new shape is compressed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: bound the handles the service files, and the shapes with them

A platform may mint a new handle each time it shows a shape, as Chrome
and Electron do, so cached_cursor_data grew by an entry per handle for
the life of the service, even when every one named the same shape; and
the compressed shapes in CURSOR_SHAPES were bounded by nothing but the
handles. Past 4096 handles, or 32 MiB of compressed shapes however few
the handles, both start over together, keeping the shape being sent: a
handle shown again is captured and named again. On every platform; the
DRM build's own 64-entry ceiling is unchanged and comes first there.

CURSOR_SHAPES keeps the bytes it holds beside the shapes, cleared with
them, so every path that clears it resets the count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: let the macOS seed report a change, and the content name the cursor

macOS named a cursor by a fingerprint of its size, hotspot and two
pixels near the hotspot. It collides between standard cursors: the open
and the closed hand give one value, and the arrows with a badge (copy,
not allowed, contextual menu, disappearing item) another, so a switch
between them was never sent, or a cached shape of the other was.
CGSCurrentCursorSeed already says when the cursor changed; get_cursor
now reports the seed, and the shape is named by its content, as on
every platform. The fingerprint is gone.

get_cursor_data checks the seed before and after the capture. The seed
was taken before capturing, so a capture that found the cursor changed
again lost the change; it now leaves it for the next poll.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: file no macOS seeds, and send nothing for the shape already shown

A macOS seed marks a change and never comes back, so filing it as a
handle only filled the map; the byte ceiling still bounds the shapes
there. A new handle or seed for the shape already shown, which a seed
change without a new shape or an app minting handles gives, sent every
connection a cursor_id for what it already showed; a shape is one
message however many handles name it, so the same message is not sent
again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: capture the macOS cursor shown without checking the seed around it

Every macOS seed change is now captured, pixel by pixel, and a seed
that moved during that capture failed it; the service then logged an
error and slept a second, so the controller's cursor lagged while it
changed. The check guarded nothing: the shape is named by its content,
not the seed, and a change after get_cursor read the seed moves it on,
so the next poll captures it anyway. Resetting the seed on a failed
check was not needed for the same reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: fall back to the default cursor for a shape the core cannot give

The shape shown before stays while the one in use decodes, so a switch
does not flash the default cursor. It also stayed once the shape in use
was known to be lost (rejected by the core, or not decoding): the
controller kept drawing, say, an I-beam for a pointer it never got,
until the peer changed shape. Both the native and the painted cursor
now leave the default one, as master did for the native cursor. A shape
that does not decode is marked in updateCursorData, whether it was sent
or fetched again, so both paths agree.

The client also selects a shape it rejects as it arrives. It showed the
shape before until the peer selected the rejected one again, and the
default cursor from then on; it now looks the same both times. Sciter
ignores an id it has no shape for, as it did.

The default and forbidden cursors are made with the model. They were
made the first time a build asked for one, and their decode lands
later with nothing to draw again, so that first build showed the shape
before in their place.

A fetch failing after the session was cleared marks nothing in the new
one, and a fetch that throws at once tells the listeners after the
build that asked for it, not during it.

The forbidden cursor, shown while input is off, became the cursor shown
last, so a raster made after input came back showed it until the
shape's pixels were fetched, or for good if they never came. Only a
shape stands in for a shape now; CursorModel.shown keeps the rule for
both builders.

_checkView asks for a single registration again: every native cursor
is made once per session, and one made twice leaks an HCURSOR on
Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: name a handle by the shape rejected under it, not the one before

A shape the client rejects is selected as it arrives, so that it shows
the default cursor then as it does later. Under a handle reused for it,
the dedupe still named the shape the handle had brought before, and
selecting that switched the pointer to an old shape: A under handle 7,
then B under 8, then a rejected C under 7 showed A in place of B, and
again whenever the peer selected 7.

The handle now names the rejected content, which is never marked
decoded, since only a shape not decoded before reaches the decode: the
UI has no shape under it and shows the default cursor, then and when
the handle is selected again. The shape the handle named before stays
kept for the other handles naming it, and a handle that brings it again
names it again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: file no handle for a shape that is rejected under it

A shape rejected under a handle named the handle by the rejected
content, so that a known handle does not show the shape it brought
before. A handle never seen was filed the same way, so a peer sending
shapes that do not decode under new handles grew the map with each,
where a rejected shape used to leave nothing. Only a known handle is
renamed now; one never seen is not filed and is passed on as unknown,
which the UI shows as the default cursor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

* cursor: say what Future.sync defers, and give the default cursor time on a slow runner

The comment on the fetch in restorePixels read as if the fetch were put
off; it runs at once, and Future.sync only turns its throw into an error
the await hands over later, which is what keeps the notify out of the
build.

cursor_default_test gave the default cursor 100 ms to decode, which a
slow runner may miss. It cannot wait for the cursor itself, since asking
for it would make it; it now waits 500 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ49AbZJYfm8NTp5yDPMab

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 12:09:46 +08:00

2628 lines
111 KiB
YAML

name: Build the flutter version of the RustDesk
on:
workflow_call:
inputs:
upload-artifact:
type: boolean
default: true
upload-tag:
type: string
default: "nightly"
# NOTE: F-Droid builder script 'flutter/build_fdroid.sh' reads environment
# variables from this workflow!
#
# It does NOT read build steps, however, so please fix 'flutter/build_fdroid.sh
# whenever you add changes to Android CI build action ('build-rustdesk-android')
# in this file!
env:
SCITER_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html
RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html
MAC_RUST_VERSION: "1.81" # 1.81 is requred for macos, because of https://github.com/yury/cidre requires 1.81
CARGO_NDK_VERSION: "3.1.2"
SCITER_ARMV7_CMAKE_VERSION: "3.29.7"
SCITER_NASM_DEBVERSION: "2.15.05-1"
LLVM_VERSION: "15.0.6"
FLUTTER_VERSION: "3.24.5"
ANDROID_FLUTTER_VERSION: "3.24.5"
# Windows arm64 only: the first stable Flutter to ship a native arm64 Windows Dart SDK +
# engine is 3.44. Every other platform stays on FLUTTER_VERSION (3.24.5) until Windows 7
# support is restored after the upstream-wide Flutter bump. The arm64 job patches the few
# 3.44-only source/pubspec changes on the fly (see "Patch RustDesk sources for Flutter 3.44").
FLUTTER_WINDOWS_ARM_VERSION: "3.44.9"
# for arm64 linux because official Dart SDK does not work
FLUTTER_ELINUX_VERSION: "3.16.9"
TAG_NAME: "${{ inputs.upload-tag }}"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
# vcpkg version: 2026.07.29
# If we change the `VCPKG COMMIT_ID`, please remember:
# 1. Call `$VCPKG_ROOT/vcpkg x-update-baseline` to update the baseline in `vcpkg.json`.
# Or we may face build issue like
# https://github.com/rustdesk/rustdesk/actions/runs/14414119794/job/40427970174
# 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`.
VCPKG_COMMIT_ID: "9e593bb18ea69cc5095e012465dcd675a822ed0d"
VCPKG_CMAKE_VERSION: "4.3.0"
ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version
VERSION: "1.5.0"
NDK_VERSION: "r28c"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}"
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}-2"
jobs:
generate-sbom:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
submodules: recursive
- name: Install Syft
uses: anchore/sbom-action/download-syft@v0
- name: Generate SBOM
run: |
syft dir:. \
-o cyclonedx-json=rustdesk.sbom.json
- name: Publish Release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
if: env.UPLOAD_ARTIFACT == 'true'
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
rustdesk.sbom.json
generate-bridge:
uses: ./.github/workflows/bridge.yml
build-RustDeskTempTopMostWindow:
uses: ./.github/workflows/third-party-RustDeskTempTopMostWindow.yml
strategy:
fail-fast: false
matrix:
job:
- {
target: windows-2022,
platform: x64,
}
- {
target: windows-11-arm,
platform: ARM64,
}
with:
upload-artifact: ${{ inputs.upload-artifact }}
target: ${{ matrix.job.target }}
configuration: Release
platform: ${{ matrix.job.platform }}
target_version: Windows10
build-for-windows-flutter:
name: ${{ matrix.job.target }}
needs: [build-RustDeskTempTopMostWindow, generate-bridge]
runs-on: ${{ matrix.job.os }}
strategy:
fail-fast: false
matrix:
job:
# - { target: i686-pc-windows-msvc , os: windows-2022 }
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
- {
target: x86_64-pc-windows-msvc,
os: windows-2022,
arch: x86_64,
flutter-arch: x64,
vcpkg-triplet: x64-windows-static,
build-args: "--vram",
}
- {
target: aarch64-pc-windows-msvc,
os: windows-11-arm,
arch: aarch64,
flutter-arch: arm64,
vcpkg-triplet: arm64-windows-static,
# vram is x86/x64-only (NVENC needs CUDA, Intel MediaSDK needs __rdtsc);
# no NV/Intel/AMD hardware exists on Windows-on-ARM, so vram stays disabled here.
build-args: "",
}
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
submodules: recursive
- name: Restore bridge files
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
# arm64 is on Flutter 3.44, so it needs the bridge generated with the same Flutter
# (its *.freezed.dart must match the freezed the arm64 build resolves). x64 and every
# other platform keep the default 3.22.3-generated bridge.
name: ${{ matrix.job.arch == 'aarch64' && 'bridge-artifact-flutter-3.44' || 'bridge-artifact' }}
path: ./
- name: Install LLVM and Clang
uses: KyleMayes/install-llvm-action@ebc0426251bc40c7cd31162802432c68818ab8f0 # v2.0.9
with:
version: ${{ env.LLVM_VERSION }}
- name: Install flutter
id: flutter
# arm64 builds with FLUTTER_WINDOWS_ARM_VERSION (>=3.44); x64 stays on FLUTTER_VERSION.
# subosito only ships an x64 Windows SDK (Flutter's release manifest lists x64 only),
# so it installs x64 on both arches. The arm64 runner re-bootstraps Dart to arm64 in
# the next step.
uses: subosito/flutter-action@2783a3f08e1baf891508463f8c6653c258246225 # v2.12.0; https://github.com/subosito/flutter-action/issues/277
with:
channel: "stable"
flutter-version: ${{ matrix.job.arch == 'aarch64' && env.FLUTTER_WINDOWS_ARM_VERSION || env.FLUTTER_VERSION }}
architecture: x64
- name: Force arm64 Dart SDK + engine
# The x64 SDK subosito installs bundles an x64 Dart with a matching engine-dart-sdk.stamp,
# so update_dart_sdk.ps1 short-circuits (stamp matches -> return) and keeps x64 Dart;
# `flutter build windows` then targets the Dart VM's arch = x64, even on this arm64 host.
# On this native-arm64 runner (PROCESSOR_ARCHITECTURE=ARM64), deleting the stamp and
# re-running update_dart_sdk.ps1 pulls the arm64 Dart (available since Flutter 3.44.0).
# https://github.com/flutter/flutter/issues/186730#issuecomment-4573214964
if: ${{ matrix.job.arch == 'aarch64' }}
run: |
$flutterRoot = "${{ steps.flutter.outputs['CACHE-PATH'] }}"
Write-Host "PROCESSOR_ARCHITECTURE=$env:PROCESSOR_ARCHITECTURE"
Write-Host "Flutter root: $flutterRoot"
Remove-Item -Force "$flutterRoot\bin\cache\engine-dart-sdk.stamp" -ErrorAction SilentlyContinue
& "$flutterRoot\bin\internal\update_dart_sdk.ps1"
# Confirm the Dart we ended up with is arm64 ("on windows_arm64"); fail loudly if not.
$dartVer = & "$flutterRoot\bin\dart.bat" --version 2>&1 | Out-String
Write-Host $dartVer
if ($dartVer -notmatch "windows_arm64") {
Write-Error "Expected an arm64 Dart SDK but got: $dartVer"
exit 1
}
& "$flutterRoot\bin\flutter.bat" precache --windows
# Fail fast if precache pulled the wrong-arch Windows engine: an arm64 Dart should
# fetch windows-arm64 engine artifacts. Bailing here saves the ~25min Rust build.
$engineDir = "$flutterRoot\bin\cache\artifacts\engine"
Write-Host "Engine artifacts present:"
Get-ChildItem $engineDir -Directory | Select-Object -ExpandProperty Name | Write-Host
if (-not (Test-Path "$engineDir\windows-arm64-release")) {
Write-Error "Expected windows-arm64-release engine artifacts but they are missing (wrong-arch SDK)."
exit 1
}
# https://github.com/flutter/flutter/issues/155685
# x64 only: arm64 uses the stock native arm64 Windows engine, and the rustdesk/engine
# windows-x64-release.zip is built for the 3.24-era x64 engine (matches FLUTTER_VERSION).
- name: Replace engine with rustdesk custom flutter engine
if: ${{ matrix.job.arch == 'x86_64' }}
run: |
flutter doctor -v
flutter precache --windows
Invoke-WebRequest -Uri https://github.com/rustdesk/engine/releases/download/main/windows-x64-release.zip -OutFile windows-x64-release.zip
Expand-Archive -Path windows-x64-release.zip -DestinationPath windows-x64-release
mv -Force windows-x64-release/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/
- name: Patch flutter
# x64 stays on Flutter 3.24.5, which needs the dropdown filter patch.
# arm64 is on Flutter 3.44 (patched separately below) and does not use this patch.
if: ${{ matrix.job.arch == 'x86_64' }}
shell: bash
run: |
cp .github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff $(dirname $(dirname $(which flutter)))
cd $(dirname $(dirname $(which flutter)))
if [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]]; then
git apply flutter_3.24.4_dropdown_menu_enableFilter.diff
fi
- name: Patch RustDesk sources for Flutter 3.44 (arm64)
# arm64 is the only target on Flutter 3.44; apply its source/pubspec deltas on the fly
# (shared with the 3.44 bridge job) so the committed sources stay on Flutter 3.24.5.
# `flutter build` then runs `pub get`, regenerating pubspec.lock for the bumped deps.
if: ${{ matrix.job.arch == 'aarch64' }}
shell: bash
run: bash .github/patches/apply_flutter_3.44_source_patches.sh
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: ${{ env.SCITER_RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
prefix-key: ${{ matrix.job.os }}
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
with:
vcpkgDirectory: C:\vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
doNotCache: false
- name: Install vcpkg dependencies
env:
VCPKG_DEFAULT_HOST_TRIPLET: ${{ matrix.job.vcpkg-triplet }}
run: |
if ! $VCPKG_ROOT/vcpkg \
install \
--triplet ${{ matrix.job.vcpkg-triplet }} \
--x-install-root="$VCPKG_ROOT/installed"; then
find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
echo "$_1:"
echo "======"
cat "$_1"
echo "======"
echo ""
done
exit 1
fi
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
shell: bash
- name: Set SODIUM_LIB_DIR (arm64)
# libsodium-sys ships no arm64 Windows prebuilt lib; point it at the vcpkg-built one
# (only for arm64 — leaving it unset lets x64 use the crate's bundled lib).
if: ${{ matrix.job.arch == 'aarch64' }}
shell: bash
run: echo "SODIUM_LIB_DIR=$VCPKG_ROOT/installed/${{ matrix.job.vcpkg-triplet }}/lib" >> "$GITHUB_ENV"
- name: Build rustdesk
run: |
# Windows: build RustDesk
# --hwcodec is shared by all Windows targets; per-target extras (e.g. --vram) come from the matrix
python3 .\build.py --portable --flutter --skip-portable-pack --hwcodec ${{ matrix.job.build-args }}
mv ./flutter/build/windows/${{ matrix.job.flutter-arch }}/runner/Release ./rustdesk
# Download usbmmidd_v2.zip and extract it to ./rustdesk
Invoke-WebRequest -Uri https://github.com/rustdesk-org/rdev/releases/download/usbmmidd_v2/usbmmidd_v2.zip -OutFile usbmmidd_v2.zip
Expand-Archive usbmmidd_v2.zip -DestinationPath .
Remove-Item -Path usbmmidd_v2\Win32 -Recurse
Remove-Item -Path "usbmmidd_v2\deviceinstaller64.exe", "usbmmidd_v2\deviceinstaller.exe", "usbmmidd_v2\usbmmidd.bat"
mv -Force .\usbmmidd_v2 ./rustdesk
# Download printer driver files and extract them to ./rustdesk
try {
Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/rustdesk_printer_driver_v4-1.4.zip -OutFile rustdesk_printer_driver_v4-1.4.zip
Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/printer_driver_adapter.zip -OutFile printer_driver_adapter.zip
Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/sha256sums -OutFile sha256sums
# Check and move the files
$checksum_driver = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*rustdesk_printer_driver_v4-1.4\.zip$').Matches.Groups[1].Value
$downloadsum_driver = Get-FileHash -Path rustdesk_printer_driver_v4-1.4.zip -Algorithm SHA256
$checksum_adapter = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*printer_driver_adapter\.zip$').Matches.Groups[1].Value
$downloadsum_adapter = Get-FileHash -Path printer_driver_adapter.zip -Algorithm SHA256
if ($checksum_driver -eq $downloadsum_driver.Hash -and $checksum_adapter -eq $downloadsum_adapter.Hash) {
Write-Output "rustdesk_printer_driver_v4-1.4, checksums match, extract the file."
Expand-Archive rustdesk_printer_driver_v4-1.4.zip -DestinationPath .
mkdir ./rustdesk/drivers
mv -Force .\rustdesk_printer_driver_v4-1.4 ./rustdesk/drivers/RustDeskPrinterDriver
Expand-Archive printer_driver_adapter.zip -DestinationPath .
mv -Force .\printer_driver_adapter.dll ./rustdesk
} elseif ($checksum_driver -ne $downloadsum_driver.Hash) {
Write-Output "rustdesk_printer_driver_v4-1.4, checksums do not match, ignore the file."
} else {
Write-Output "printer_driver_adapter.dll, checksums do not match, ignore the file."
}
} catch {
Write-Host "Ingore the printer driver error."
}
- name: find Runner.res
# Windows: find Runner.res (compiled from ./flutter/windows/runner/Runner.rc), copy to ./Runner.res
# Runner.rc does not contain actual version, but Runner.res does
continue-on-error: true
shell: bash
run: |
runner_res=$(find . -name "Runner.res");
if [ "$runner_res" == "" ]; then
echo "Runner.res: not found";
else
echo "Runner.res: $runner_res";
cp $runner_res ./libs/portable/Runner.res;
echo "list ./libs/portable/Runner.res";
ls -l ./libs/portable/Runner.res;
fi
- name: Download RustDeskTempTopMostWindow artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
if: ${{ inputs.upload-artifact }}
with:
name: ${{ matrix.job.arch == 'aarch64' && 'topmostwindow-artifacts-ARM64' || 'topmostwindow-artifacts-x64' }}
path: "./rustdesk"
- name: Upload unsigned
if: env.UPLOAD_ARTIFACT == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: rustdesk-unsigned-windows-${{ matrix.job.arch }}
path: rustdesk
- name: Sign rustdesk files
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2'
shell: bash
run: |
pip3 install requests argparse
BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./rustdesk/
- name: Build self-extracted executable
shell: bash
if: env.UPLOAD_ARTIFACT == 'true'
run: |
sed -i '/dpiAware/d' res/manifest.xml
pushd ./libs/portable
pip3 install -r requirements.txt
python3 ./generate.py -f ../../rustdesk/ -o . -e ../../rustdesk/rustdesk.exe
popd
mkdir -p ./SignOutput
mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.exe
- name: Add MSBuild to PATH
uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2
with:
# Select the MSBuild process architecture; -p:Platform sets the MSI target.
# Native ARM64 tools give the compiler more address space for PCH files
# (C3859/C1076 were reported by HostX86\arm64\CL.exe).
# Keep the action's default x86 MSBuild for the existing x64 MSI job.
msbuild-architecture: ${{ matrix.job.arch == 'aarch64' && 'arm64' || 'x86' }}
- name: Build msi
# Builds the MSI for the matrix arch. res/msi (WiX v4 + native CustomActions) carries
# both x64 and ARM64 platform configs; WcaUtil/DUtil ship arm64 libs. msbuild platform
# is x64 / ARM64; the produced Package.msi is globbed since its bin/<platform>/ dir varies.
if: env.UPLOAD_ARTIFACT == 'true'
run: |
pushd ./res/msi
python preprocess.py --arp -d ../../rustdesk
nuget restore msi.sln
$msiPlatform = if ('${{ matrix.job.arch }}' -eq 'aarch64') { 'ARM64' } else { 'x64' }
msbuild msi.sln -p:Configuration=Release -p:Platform=$msiPlatform /p:TargetVersion=Windows10
$msi = Get-ChildItem ./Package/bin/*/Release/en-us/Package.msi | Select-Object -First 1
mv $msi.FullName ../../SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.msi
sha256sum ../../SignOutput/rustdesk-*.msi
- name: Build pre-built MSI template
# Two things this works around: preprocess.py rewrites res/msi in place, so the
# tree is reset around this second variant; and it locates the app as
# <app-name>.exe inside the dist, so the dist copy is renamed to match.
#
# The placeholder is chosen to keep this template as close to the shipped msi as
# possible: eight characters like "RustDesk", and a valid 8.3 name, so WiX
# derives no short name for it. A longer placeholder would get one, and a patch
# cannot rewrite a truncated placeholder, leaving short names pointing at it.
#
# It still has to be unique, which is why "RustDesk" itself cannot be used:
# it also names payload that must never be renamed, such as librustdesk.dll
# and drivers\RustDeskPrinterDriver.
#
#
# Building the arm64 template on the native arm64 runner makes the ARM
# package available: the build agents are x64 and cannot run
# preprocess.py against an ARM exe.
if: env.UPLOAD_ARTIFACT == 'true'
run: |
git checkout -- res/msi
cp -r ./rustdesk ./rustdesk-msi-template
mv ./rustdesk-msi-template/rustdesk.exe ./rustdesk-msi-template/RDAPPNAM.exe
Set-Content -Path ./rustdesk-msi-template/custom.txt -Value 'placeholder' -NoNewline
$assets = './rustdesk-msi-template/data/flutter_assets/assets'
New-Item -ItemType Directory -Force -Path $assets | Out-Null
foreach ($a in 'icon.ico','icon.png','logo.png','logo_light.png','logo_dark.png') {
Set-Content -Path "$assets/$a" -Value 'placeholder' -NoNewline
}
pushd ./res/msi
python preprocess.py --arp --template --revision-version 0 -d ../../rustdesk-msi-template --app-name RDAPPNAM
$msiPlatform = if ('${{ matrix.job.arch }}' -eq 'aarch64') { 'ARM64' } else { 'x64' }
msbuild msi.sln -t:clean -p:Configuration=Release -p:Platform=$msiPlatform
msbuild msi.sln -p:Configuration=Release -p:Platform=$msiPlatform /p:TargetVersion=Windows10
$msi = Get-ChildItem ./Package/bin/*/Release/en-us/Package.msi | Select-Object -First 1
popd
mkdir ./msi-template
mv $msi.FullName ./msi-template/rustdesk-template-${{ matrix.job.arch }}.msi
git checkout -- res/msi
rm -r -fo ./rustdesk-msi-template
- name: Upload unsigned msi template
if: env.UPLOAD_ARTIFACT == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: rustdesk-unsigned-msi-template-${{ matrix.job.arch }}
path: ./msi-template
- name: Sign rustdesk self-extracted file
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2'
shell: bash
run: |
BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput
- name: Publish Release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
if: env.UPLOAD_ARTIFACT == 'true'
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
./SignOutput/rustdesk-*.msi
./SignOutput/rustdesk-*.exe
# The fallback for the flutter version, we use Sciter for 32bit Windows.
build-for-windows-sciter:
name: ${{ matrix.job.target }} (${{ matrix.job.os }})
runs-on: ${{ matrix.job.os }}
# Temporarily disable this action due to additional test is needed.
# if: false
strategy:
fail-fast: false
matrix:
job:
# - { target: i686-pc-windows-msvc , os: windows-2022 }
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
- {
target: i686-pc-windows-msvc,
os: windows-2022,
arch: x86,
vcpkg-triplet: x86-windows-static,
}
# - { target: aarch64-pc-windows-msvc, os: windows-2022 }
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
submodules: recursive
- name: Install LLVM and Clang
uses: rustdesk-org/install-llvm-action-32bit@6aa7d9ad3df84dff01cd4596dd0fc880a7f47fce # no release tag; commit 2026-05-26
with:
version: ${{ env.LLVM_VERSION }}
- name: Install Rust toolchain
shell: bash
run: |
# Sciter's abi_thiscall feature requires nightly Rust.
# Use an i686 host toolchain so build scripts can load the 32-bit LLVM installed above.
# Since rustup 1.29.1, i686 toolchains on x64 Windows require --force-non-host
# for both installation and default selection, even though WOW64 can run them.
# See https://github.com/rust-lang/rustup/pull/4935.
# Alternatively, an x64 compiler could use cargo build --target i686-pc-windows-msvc.
# This requires 64-bit LLVM for host build scripts and updated packaging paths,
# and has not been manually verified for RustDesk.
rustup toolchain install nightly-2023-10-13-${{ matrix.job.target }} \
--target ${{ matrix.job.target }} --component rustfmt \
--profile minimal --no-self-update --force-non-host
rustup default nightly-2023-10-13-${{ matrix.job.target }} --force-non-host
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
prefix-key: ${{ matrix.job.os }}-sciter
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
with:
vcpkgDirectory: C:\vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
doNotCache: false
- name: Install vcpkg dependencies
env:
VCPKG_DEFAULT_HOST_TRIPLET: ${{ matrix.job.vcpkg-triplet }}
run: |
if ! $VCPKG_ROOT/vcpkg \
install \
--triplet ${{ matrix.job.vcpkg-triplet }} \
--x-install-root="$VCPKG_ROOT/installed"; then
find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
echo "$_1:"
echo "======"
cat "$_1"
echo "======"
echo ""
done
exit 1
fi
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
shell: bash
- name: Build rustdesk
id: build
shell: bash
run: |
python3 res/inline-sciter.py
# Patch sciter x86
sed -i 's/branch = "dyn"/branch = "dyn_x86"/g' ./Cargo.toml
cargo update -p sciter-rs --precise 674e07d3066ca9a92ced3816203ab6b652629d1e
cargo build --locked --features inline,vram,hwcodec --release --bins
mkdir -p ./Release
mv ./target/release/rustdesk.exe ./Release/rustdesk.exe
curl -LJ -o ./Release/sciter.dll https://github.com/c-smile/sciter-sdk/raw/master/bin.win/x32/sciter.dll
echo "output_folder=./Release" >> $GITHUB_OUTPUT
curl -LJ -o ./usbmmidd_v2.zip https://github.com/rustdesk-org/rdev/releases/download/usbmmidd_v2/usbmmidd_v2.zip
unzip usbmmidd_v2.zip
# Do not remove x64 files, because the user may run the 32bit version on a 64bit system.
# Do not remove ./usbmmidd_v2/deviceinstaller64.exe, as x86 exe cannot install and uninstall drivers when running on x64,
# we need the x64 exe to install and uninstall the driver.
rm -rf ./usbmmidd_v2/deviceinstaller.exe ./usbmmidd_v2/usbmmidd.bat
mv ./usbmmidd_v2 ./Release || true
- name: find Runner.res
# Windows: find Runner.res (compiled from ./flutter/windows/runner/Runner.rc), copy to ./Runner.res
# Runner.rc does not contain actual version, but Runner.res does
continue-on-error: true
shell: bash
run: |
runner_res=$(find . -name "Runner.res");
if [ "$runner_res" == "" ]; then
echo "Runner.res: not found";
else
echo "Runner.res: $runner_res";
cp $runner_res ./libs/portable/Runner.res;
echo "list ./libs/portable/Runner.res";
ls -l ./libs/portable/Runner.res;
fi
- name: Upload unsigned
if: env.UPLOAD_ARTIFACT == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: rustdesk-unsigned-windows-${{ matrix.job.arch }}
path: Release
- name: Sign rustdesk files
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2'
shell: bash
run: |
pip3 install requests argparse
BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./Release/
- name: Build self-extracted executable
shell: bash
run: |
sed -i '/dpiAware/d' res/manifest.xml
pushd ./libs/portable
pip3 install -r requirements.txt
python3 ./generate.py -f ../../Release/ -o . -e ../../Release/rustdesk.exe
popd
mkdir -p ./SignOutput
mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.exe
- name: Sign rustdesk self-extracted file
if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2'
shell: bash
run: |
BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput/
- name: Publish Release
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
if: env.UPLOAD_ARTIFACT == 'true'
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
./SignOutput/rustdesk-*.exe
build-rustdesk-ios:
if: ${{ inputs.upload-artifact }}
name: build rustdesk ios ipa
runs-on: ${{ matrix.job.os }}
needs: [generate-bridge]
strategy:
fail-fast: false
matrix:
job:
- {
arch: aarch64,
target: aarch64-apple-ios,
os: macos-latest,
vcpkg-triplet: arm64-ios,
}
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Install dependencies
run: |
brew install nasm yasm
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
submodules: recursive
- name: Install flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Patch flutter
run: |
cd $(dirname $(dirname $(which flutter)))
if [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]]; then
git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
fi
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
with:
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
doNotCache: false
- name: Install vcpkg dependencies
run: |
if ! $VCPKG_ROOT/vcpkg \
install \
--triplet ${{ matrix.job.vcpkg-triplet }} \
--x-install-root="$VCPKG_ROOT/installed"; then
find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
echo "$_1:"
echo "======"
cat "$_1"
echo "======"
echo ""
done
exit 1
fi
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
shell: bash
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
prefix-key: rustdesk-lib-cache-ios
key: ${{ matrix.job.target }}
- name: Restore bridge files
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: bridge-artifact
path: ./
- name: Build rustdesk lib
run: |
rustup target add ${{ matrix.job.target }}
cargo build --locked --features flutter,hwcodec --release --target aarch64-apple-ios --lib
- name: Upload liblibrustdesk.a Artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: liblibrustdesk.a
path: target/aarch64-apple-ios/release/liblibrustdesk.a
- name: Build rustdesk
shell: bash
run: |
pushd flutter
# flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info --no-codesign
# for easy debugging
flutter build ipa --release --no-codesign
# - name: Upload Artifacts
# # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
# uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
# with:
# name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
# path: flutter/build/ios/ipa/*.ipa
# - name: Publish ipa package
# # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
# uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
# with:
# prerelease: true
# tag_name: ${{ env.TAG_NAME }}
# files: |
# flutter/build/ios/ipa/*.ipa
build-for-macOS:
name: ${{ matrix.job.target }}
runs-on: ${{ matrix.job.os }}
needs: [generate-bridge]
strategy:
fail-fast: false
matrix:
job:
- {
target: x86_64-apple-darwin,
os: macos-15-intel, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel
extra-build-args: "",
arch: x86_64,
vcpkg-triplet: x64-osx,
}
- {
target: aarch64-apple-darwin,
os: macos-14,
# extra-build-args: "--disable-flutter-texture-render", # disable this for mac, because we see a lot of users reporting flickering both on arm and x64, and we can not confirm if texture rendering has better performance if htere is no vram, https://github.com/rustdesk/rustdesk/issues/6296
extra-build-args: "--screencapturekit",
arch: aarch64,
vcpkg-triplet: arm64-osx,
}
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
submodules: recursive
- name: Import the codesign cert
if: env.MACOS_P12_BASE64 != null
uses: apple-actions/import-codesign-certs@253ddeeac23f2bdad1646faac5c8c2832e800071 # v1
with:
p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }}
p12-password: ${{ secrets.MACOS_P12_PASSWORD }}
keychain: rustdesk
- name: Check sign and import sign key
if: env.MACOS_P12_BASE64 != null
run: |
security default-keychain -s rustdesk.keychain
security find-identity -v
- name: Import notarize key
if: env.MACOS_P12_BASE64 != null
uses: timheuer/base64-to-file@adaa40c0c581f276132199d4cf60afa07ce60eac # v1.2
with:
# https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling
fileName: rustdesk.json
fileDir: ${{ github.workspace }}
encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }}
- name: Install rcodesign tool
if: env.MACOS_P12_BASE64 != null
shell: bash
run: |
pushd /tmp
wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz
tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz
mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin
popd
- name: Install build runtime
run: |
brew install llvm create-dmg
# pkg-config is handled in a separate step, because it may be already installed by `macos-latest`(14.7.1) runner
if command -v pkg-config &>/dev/null; then
echo "pkg-config is already installed"
else
brew install pkg-config
fi
- name: Install NASM
run: |
# Install NASM 2.16.x from official release.
# Do NOT use `brew install nasm` which installs NASM 3.x.
# NASM 3.x is a complete rewrite with incompatible CLI options and removed features.
# aom and other multimedia libraries require NASM 2.x for x86/x86_64 assembly.
wget https://www.nasm.us/pub/nasm/releasebuilds/2.16.03/macosx/nasm-2.16.03-macosx.zip
unzip nasm-2.16.03-macosx.zip
sudo cp nasm-2.16.03/nasm /usr/local/bin/nasm
nasm --version
- name: Install flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Patch flutter
run: |
cd $(dirname $(dirname $(which flutter)))
if [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]]; then
git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
fi
- name: Workaround for flutter issue
shell: bash
run: |
cd "$(dirname "$(which flutter)")"
# https://github.com/flutter/flutter/issues/133533
sed -i -e 's/_setFramesEnabledState(false);/\/\/_setFramesEnabledState(false);/g' ../packages/flutter/lib/src/scheduler/binding.dart
grep -n '_setFramesEnabledState(false);' ../packages/flutter/lib/src/scheduler/binding.dart
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: ${{ env.MAC_RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
prefix-key: ${{ matrix.job.os }}
- name: Restore bridge files
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: bridge-artifact
path: ./
- name: Test cursor sizing
if: ${{ !inputs.upload-artifact && matrix.job.arch == 'aarch64' }}
working-directory: flutter
run: flutter test --timeout 60s test/cursor_size_test.dart test/cursor_replay_test.dart test/cursor_default_test.dart
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
with:
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
doNotCache: false
- name: Install vcpkg dependencies
run: |
if ! $VCPKG_ROOT/vcpkg \
install \
--x-install-root="$VCPKG_ROOT/installed"; then
find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
echo "$_1:"
echo "======"
cat "$_1"
echo "======"
echo ""
done
exit 1
fi
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
- name: Show version information (Rust, cargo, Clang)
shell: bash
run: |
clang --version || true
rustup -V
rustup toolchain list
rustup default
cargo -V
rustc -V
- name: Build rustdesk
run: |
if [ "${{ matrix.job.target }}" = "aarch64-apple-darwin" ]; then
MIN_MACOS_VERSION="12.3"
sed -i -e "s/MACOSX_DEPLOYMENT_TARGET\=[0-9]*.[0-9]*/MACOSX_DEPLOYMENT_TARGET=${MIN_MACOS_VERSION}/" build.py
sed -i -e "s/platform :osx, '.*'/platform :osx, '${MIN_MACOS_VERSION}'/" flutter/macos/Podfile
sed -i -e "s/osx_minimum_system_version = \"[0-9]*.[0-9]*\"/osx_minimum_system_version = \"${MIN_MACOS_VERSION}\"/" Cargo.toml
sed -i -e "s/MACOSX_DEPLOYMENT_TARGET = [0-9]*.[0-9]*;/MACOSX_DEPLOYMENT_TARGET = ${MIN_MACOS_VERSION};/" flutter/macos/Runner.xcodeproj/project.pbxproj
fi
./build.py --flutter --hwcodec --unix-file-copy-paste ${{ matrix.job.extra-build-args }}
- name: create unsigned dmg
if: env.UPLOAD_ARTIFACT == 'true'
run: |
CREATE_DMG="$(command -v create-dmg)"
CREATE_DMG="$(readlink -f "$CREATE_DMG")"
sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG"
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
- name: Upload unsigned macOS app
if: env.UPLOAD_ARTIFACT == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: rustdesk-unsigned-macos-${{ matrix.job.arch }}
path: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.dmg # can not upload the directory directly or tar.gz, which destroy the link structure, causing the codesign failed
- name: Codesign app and create signed dmg
if: env.MACOS_P12_BASE64 != null && env.UPLOAD_ARTIFACT == 'true'
run: |
# Patch create-dmg to give more attempts to unmount image
CREATE_DMG="$(command -v create-dmg)"
CREATE_DMG="$(readlink -f "$CREATE_DMG")"
sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG"
# Unlock keychain
security default-keychain -s rustdesk.keychain
security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain
# start sign the rustdesk.app and dmg
rm -rf *.dmg || true
# the identity secret carries its own shell quoting, so expand it inline like the dmg codesign below
bash ./.github/scripts/sign-macos-app.sh \
./flutter/build/macos/Build/Products/Release/RustDesk.app \
${{ secrets.MACOS_CODESIGN_IDENTITY }} \
./flutter/macos/Runner/Release.entitlements
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv
# notarize the rustdesk-${{ env.VERSION }}.dmg
rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg
- name: Rename rustdesk
if: env.UPLOAD_ARTIFACT == 'true'
run: |
for name in rustdesk*??.dmg; do
mv "$name" "${name%%.dmg}-${{ matrix.job.arch }}.dmg"
done
- name: Publish DMG package
if: env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
rustdesk*-${{ matrix.job.arch }}.dmg
publish_unsigned:
needs:
- build-for-macOS
- build-for-windows-flutter
- build-for-windows-sciter
runs-on: ubuntu-latest
if: ${{ inputs.upload-artifact }}
steps:
- name: Download artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: rustdesk-unsigned-macos-x86_64
path: ./
- name: Download Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: rustdesk-unsigned-macos-aarch64
path: ./
- name: Download Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: rustdesk-unsigned-windows-x86_64
path: ./windows-x86_64/
- name: Download Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: rustdesk-unsigned-windows-aarch64
path: ./windows-aarch64/
- name: Download Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: rustdesk-unsigned-windows-x86
path: ./windows-x86/
- name: Download Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: rustdesk-unsigned-msi-template-x86_64
path: ./msi-template/
- name: Download Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: rustdesk-unsigned-msi-template-aarch64
path: ./msi-template/
- name: Combine unsigned app
run: |
tar czf rustdesk-${{ env.VERSION }}-unsigned.tar.gz *.dmg windows-x86_64 windows-aarch64 windows-x86 msi-template
- name: Publish unsigned app
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: rustdesk-${{ env.VERSION }}-unsigned.tar.gz
build-rustdesk-android:
needs: [generate-bridge]
name: build rustdesk android apk ${{ matrix.job.target }}
runs-on: ${{ matrix.job.os }}
strategy:
fail-fast: false
matrix:
job:
- {
arch: aarch64,
target: aarch64-linux-android,
os: ubuntu-24.04,
reltype: release,
suffix: "",
}
- {
arch: armv7,
target: armv7-linux-androideabi,
os: ubuntu-24.04,
reltype: release,
suffix: "",
}
- {
arch: x86_64,
target: x86_64-linux-android,
os: ubuntu-24.04,
reltype: release,
suffix: "",
}
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
with:
tool-cache: false
android: false
dotnet: true
haskell: true
large-packages: false
docker-images: true
swap-storage: false
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
clang \
cmake \
curl \
gcc-multilib \
git \
g++ \
g++-multilib \
libayatana-appindicator3-dev \
libasound2-dev \
libc6-dev \
libclang-dev \
libunwind-dev \
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \
libpulse-dev \
libva-dev \
libxcb-randr0-dev \
libxcb-shape0-dev \
libxcb-xfixes0-dev \
libxdo-dev \
libxfixes-dev \
llvm-dev \
nasm \
ninja-build \
openjdk-17-jdk-headless \
pkg-config \
tree \
wget
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
submodules: recursive
- name: Install flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
with:
channel: "stable"
flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }}
- name: Patch flutter
run: |
cd $(dirname $(dirname $(which flutter)))
if [[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]]; then
git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
fi
- uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1
id: setup-ndk
with:
ndk-version: ${{ env.NDK_VERSION }}
add-to-path: true
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
with:
vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
doNotCache: false
- name: Install vcpkg dependencies
run: |
case ${{ matrix.job.target }} in
aarch64-linux-android)
ANDROID_TARGET=arm64-v8a
;;
armv7-linux-androideabi)
ANDROID_TARGET=armeabi-v7a
;;
x86_64-linux-android)
ANDROID_TARGET=x86_64
;;
i686-linux-android)
ANDROID_TARGET=x86
;;
esac
if ! ./flutter/build_android_deps.sh "${ANDROID_TARGET}"; then
find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
echo "$_1:"
echo "======"
cat "$_1"
echo "======"
echo ""
done
exit 1
fi
shell: bash
- name: Restore bridge files
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: bridge-artifact
path: ./
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: ${{ env.RUST_VERSION }}
components: "rustfmt"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
prefix-key: rustdesk-lib-cache-android # TODO: drop '-android' part after caches are invalidated
key: ${{ matrix.job.target }}
- name: Build rustdesk lib
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
run: |
rustup target add ${{ matrix.job.target }}
cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }} --locked
case ${{ matrix.job.target }} in
aarch64-linux-android)
./flutter/ndk_arm64.sh
mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
;;
armv7-linux-androideabi)
./flutter/ndk_arm.sh
mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
;;
x86_64-linux-android)
./flutter/ndk_x64.sh
mkdir -p ./flutter/android/app/src/main/jniLibs/x86_64
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86_64/librustdesk.so
;;
i686-linux-android)
./flutter/ndk_x86.sh
mkdir -p ./flutter/android/app/src/main/jniLibs/x86
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86/librustdesk.so
;;
esac
- name: Upload Rustdesk library to Artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: librustdesk.so.${{ matrix.job.target }}
path: ./target/${{ matrix.job.target }}/release/liblibrustdesk.so
- name: Build rustdesk
shell: bash
env:
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
run: |
export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH
# Increase Gradle JVM memory for CI builds
sed -i "s/org.gradle.jvmargs=-Xmx1024M/org.gradle.jvmargs=-Xmx2g/g" ./flutter/android/gradle.properties
# temporary use debug sign config
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
case ${{ matrix.job.target }} in
aarch64-linux-android)
mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
# build flutter
pushd flutter
flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-arm64 --split-per-abi
mv build/app/outputs/flutter-apk/app-arm64-v8a-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk
;;
armv7-linux-androideabi)
mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/arm-linux-androideabi/libc++_shared.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
# build flutter
pushd flutter
flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-arm --split-per-abi
mv build/app/outputs/flutter-apk/app-armeabi-v7a-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk
;;
x86_64-linux-android)
mkdir -p ./flutter/android/app/src/main/jniLibs/x86_64
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86_64/
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86_64/librustdesk.so
# build flutter
pushd flutter
flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-x64 --split-per-abi
mv build/app/outputs/flutter-apk/app-x86_64-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk
;;
i686-linux-android)
mkdir -p ./flutter/android/app/src/main/jniLibs/x86
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/i686-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86/
cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86/librustdesk.so
# build flutter
pushd flutter
flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-x86 --split-per-abi
mv build/app/outputs/flutter-apk/app-x86-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk
;;
esac
popd
mkdir -p signed-apk; pushd signed-apk
mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk .
# https://github.com/r0adkll/sign-android-release/issues/84#issuecomment-1889636075
- name: Setup sign tool version variable
shell: bash
run: |
BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1)
echo "ANDROID_SIGN_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
echo Last build tool version is: $BUILD_TOOL_VERSION
- uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # v1
name: Sign app APK
if: env.ANDROID_SIGNING_KEY != null
id: sign-rustdesk
with:
releaseDirectory: ./signed-apk
signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }}
alias: ${{ secrets.ANDROID_ALIAS }}
keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}
env:
# env.ANDROID_SIGN_TOOL_VERSION is set by Step "Setup sign tool version variable"
BUILD_TOOLS_VERSION: ${{ env.ANDROID_SIGN_TOOL_VERSION }}
- name: Upload Artifacts
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}}
- name: Publish signed apk package
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
${{steps.sign-rustdesk.outputs.signedReleaseFile}}
- name: Publish unsigned apk package
if: env.ANDROID_SIGNING_KEY == null && env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
signed-apk/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
build-rustdesk-android-universal:
needs: [build-rustdesk-android]
name: build rustdesk android universal apk
if: ${{ inputs.upload-artifact }}
runs-on: ubuntu-24.04
env:
reltype: release
x86_target: "" # can be ",android-x86"
suffix: ""
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
with:
tool-cache: false
android: false
dotnet: true
haskell: true
large-packages: false
docker-images: true
swap-storage: false
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
clang \
cmake \
curl \
gcc-multilib \
git \
g++ \
g++-multilib \
libayatana-appindicator3-dev \
libasound2-dev \
libc6-dev \
libclang-dev \
libunwind-dev \
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \
libpulse-dev \
libva-dev \
libxcb-randr0-dev \
libxcb-shape0-dev \
libxcb-xfixes0-dev \
libxdo-dev \
libxfixes-dev \
llvm-dev \
nasm \
ninja-build \
openjdk-17-jdk-headless \
pkg-config \
tree \
wget
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
submodules: recursive
- name: Install flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
with:
channel: "stable"
flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }}
- name: Patch flutter
run: |
cd $(dirname $(dirname $(which flutter)))
if [[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]]; then
git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
fi
- name: Restore bridge files
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: bridge-artifact
path: ./
- name: Download Rustdesk library from Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: librustdesk.so.aarch64-linux-android
path: ./flutter/android/app/src/main/jniLibs/arm64-v8a
- name: Download Rustdesk library from Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: librustdesk.so.armv7-linux-androideabi
path: ./flutter/android/app/src/main/jniLibs/armeabi-v7a
- name: Download Rustdesk library from Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: librustdesk.so.x86_64-linux-android
path: ./flutter/android/app/src/main/jniLibs/x86_64
- name: Download Rustdesk library from Artifacts
if: ${{ env.reltype == 'debug' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: librustdesk.so.i686-linux-android
path: ./flutter/android/app/src/main/jniLibs/x86
- name: Build rustdesk
shell: bash
env:
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
run: |
export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH
# Increase Gradle JVM memory for CI builds
sed -i "s/org.gradle.jvmargs=-Xmx1024M/org.gradle.jvmargs=-Xmx2g/g" ./flutter/android/gradle.properties
# temporary use debug sign config
sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
mv ./flutter/android/app/src/main/jniLibs/arm64-v8a/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/
mv ./flutter/android/app/src/main/jniLibs/armeabi-v7a/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/arm-linux-androideabi/libc++_shared.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/
mv ./flutter/android/app/src/main/jniLibs/x86_64/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86_64/librustdesk.so
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86_64/
if [ "${{ env.reltype }}" = "debug" ]; then
mv ./flutter/android/app/src/main/jniLibs/x86/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86/librustdesk.so
cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/i686-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86/
fi
# build flutter
pushd flutter
flutter build apk "--${{ env.reltype }}" --target-platform android-arm64,android-arm,android-x64${{ env.x86_target }}
popd
mkdir -p signed-apk
mv ./flutter/build/app/outputs/flutter-apk/app-${{ env.reltype }}.apk signed-apk/rustdesk-${{ env.VERSION }}-universal${{ env.suffix }}.apk
# https://github.com/r0adkll/sign-android-release/issues/84#issuecomment-1889636075
- name: Setup sign tool version variable
shell: bash
run: |
BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1)
echo "ANDROID_SIGN_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
echo Last build tool version is: $BUILD_TOOL_VERSION
- uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # v1
name: Sign app APK
if: env.ANDROID_SIGNING_KEY != null
id: sign-rustdesk
with:
releaseDirectory: ./signed-apk
signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }}
alias: ${{ secrets.ANDROID_ALIAS }}
keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}
env:
# env.ANDROID_SIGN_TOOL_VERSION is set by Step "Setup sign tool version variable"
BUILD_TOOLS_VERSION: ${{ env.ANDROID_SIGN_TOOL_VERSION }}
- name: Upload Artifacts
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}}
- name: Publish signed apk package
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
${{steps.sign-rustdesk.outputs.signedReleaseFile}}
- name: Publish unsigned apk package
if: env.ANDROID_SIGNING_KEY == null && env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
signed-apk/rustdesk-${{ env.VERSION }}-universal${{ env.suffix }}.apk
build-rustdesk-linux:
needs: [generate-bridge]
name: build rustdesk linux ${{ matrix.job.target }}
runs-on: ${{ matrix.job.on }}
strategy:
fail-fast: false
matrix:
# use a high level qemu-user-static
job:
- {
arch: x86_64,
target: x86_64-unknown-linux-gnu,
distro: ubuntu18.04,
on: ubuntu-22.04,
deb_arch: amd64,
vcpkg-triplet: x64-linux,
}
- {
arch: aarch64,
target: aarch64-unknown-linux-gnu,
distro: ubuntu18.04,
on: ubuntu-22.04-arm,
deb_arch: arm64,
vcpkg-triplet: arm64-linux,
}
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Maximize build space
run: |
sudo rm -rf /opt/ghc
sudo rm -rf /usr/local/lib/android
sudo rm -rf /usr/share/dotnet
sudo apt-get update -y
sudo apt-get install -y nasm
if [[ "${{ matrix.job.arch }}" == "x86_64" ]]; then
sudo apt-get install -y qemu-user-static
fi
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
submodules: recursive
- name: Set Swap Space
uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c # v1.0
with:
swap-size-gb: 12
- name: Free Space
run: |
df -h
free -m
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
with:
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
- name: Save Rust toolchain version
run: |
RUST_TOOLCHAIN_VERSION=$(cargo --version | awk '{print $2}')
echo "RUST_TOOLCHAIN_VERSION=$RUST_TOOLCHAIN_VERSION" >> $GITHUB_ENV
- name: Disable rust bridge build
run: |
# only build cdylib
sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml
- name: Restore bridge files
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: bridge-artifact
path: ./
# vcpkg 2026.07.29's SPDX scripts require CMake 4.3+, but this ARM64 runner selects CMake 3.31.
- name: Install CMake for vcpkg on Linux ARM64
if: matrix.job.arch == 'aarch64' && env.UPLOAD_ARTIFACT == 'true'
run: |
python3 -m pip install --user "cmake==${VCPKG_CMAKE_VERSION}"
user_base="$(python3 -m site --user-base)"
"${user_base}/bin/cmake" --version
echo "${user_base}/bin" >> "${GITHUB_PATH}"
- name: Setup vcpkg with Github Actions binary cache
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
with:
vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
doNotCache: false
- name: Install vcpkg dependencies
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
run: |
sudo apt install -y libva-dev && apt show libva-dev
if ! $VCPKG_ROOT/vcpkg \
install \
--triplet ${{ matrix.job.vcpkg-triplet }} \
--x-install-root="$VCPKG_ROOT/installed"; then
find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
echo "$_1:"
echo "======"
cat "$_1"
echo "======"
echo ""
done
exit 1
fi
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
shell: bash
- name: Restore bridge files
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: bridge-artifact
path: ./
- uses: rustdesk-org/run-on-arch-action@d3fcfbb632b84cf7f6bc772bfaaa2c2f4f8789a8 # no release tag; commit 2026-05-26
name: Build rustdesk
id: vcpkg
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
with:
arch: ${{ matrix.job.arch }}
distro: ${{ matrix.job.distro }}
githubToken: ${{ github.token }}
setup: |
ls -l "${PWD}"
ls -l /opt/artifacts/vcpkg/installed
dockerRunArgs: |
--volume "${PWD}:/workspace"
--volume "/opt/artifacts:/opt/artifacts"
shell: /bin/bash
install: |
apt-get update -y
echo -e "installing deps"
apt-get install -y \
build-essential \
clang \
cmake \
curl \
gcc \
git \
g++ \
libayatana-appindicator3-dev \
libasound2-dev \
libclang-10-dev \
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \
libpulse-dev \
libva-dev \
libxcb-randr0-dev \
libxcb-shape0-dev \
libxcb-xfixes0-dev \
libxdo-dev \
libxfixes-dev \
llvm-10-dev \
nasm \
ninja-build \
pkg-config \
tree \
python3 \
rpm \
unzip \
wget \
xz-utils \
libssl-dev
# we have libopus compiled by us.
apt-get remove -y libopus-dev || true
# output devs
ls -l ./
tree -L 3 /opt/artifacts/vcpkg/installed
run: |
# disable git safe.directory
git config --global --add safe.directory "*"
# rust
pushd /opt
# do not use rustup, because memory overflow in qemu
wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}.tar.gz
tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz
cd rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }} && ./install.sh
rm -rf rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}
# edit config
mkdir -p ~/.cargo/
echo """
[source.crates-io]
registry = 'https://github.com/rust-lang/crates.io-index'
""" > ~/.cargo/config
cat ~/.cargo/config
# start build
pushd /workspace
export VCPKG_ROOT=/opt/artifacts/vcpkg
if [[ "${{ matrix.job.arch }}" == "aarch64" ]]; then
export JOBS="--jobs 3"
else
export JOBS=""
fi
echo $JOBS
cargo build --locked --lib $JOBS --features hwcodec,flutter,unix-file-copy-paste --release
rm -rf target/release/deps target/release/build
rm -rf ~/.cargo
# Setup Flutter
# disable git safe.directory
git config --global --add safe.directory "*"
pushd /workspace
case ${{ matrix.job.arch }} in
aarch64)
export PATH=/opt/flutter-elinux/bin:$PATH
sed -i "s/flutter build linux --release/flutter-elinux build linux --verbose/g" ./build.py
sed -i "s/x64\/release/arm64\/release/g" ./build.py
;;
x86_64)
export PATH=/opt/flutter/bin:$PATH
;;
esac
popd
pushd /opt
wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz
tar xf flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz
case ${{ matrix.job.arch }} in
aarch64)
# clone repo and reset to flutter ${{ env.FLUTTER_VERSION }}
git clone https://github.com/sony/flutter-elinux.git || true
pushd flutter-elinux
git fetch
git reset --hard ${{ env.FLUTTER_VERSION }}
bin/flutter-elinux doctor -v
bin/flutter-elinux precache --linux
popd
cp -R flutter/bin/cache/artifacts/engine/linux-x64/shader_lib flutter-elinux/flutter/bin/cache/artifacts/engine/linux-arm64
rm -rf flutter
;;
x86_64)
flutter doctor -v
;;
esac
if [[ "3.24.5" == ${{ env.FLUTTER_VERSION }} ]]; then
case ${{ matrix.job.arch }} in
aarch64)
pushd /opt/flutter-elinux/flutter
;;
x86_64)
pushd /opt/flutter
;;
esac
git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
popd
fi
# build flutter
pushd /workspace
export CARGO_INCREMENTAL=0
export DEB_ARCH=${{ matrix.job.deb_arch }}
python3 ./build.py --flutter --skip-cargo
for name in rustdesk*??.deb; do
mv "$name" "${name%%.deb}-${{ matrix.job.arch }}.deb"
done
# rpm package
echo -e "start packaging fedora package"
pushd /workspace
case ${{ matrix.job.arch }} in
aarch64)
sed -i "s/linux\/x64/linux\/arm64/g" ./res/rpm-flutter.spec
;;
esac
HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb
pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }}
for name in rustdesk*??.rpm; do
mv "$name" /workspace/"${name%%.rpm}.rpm"
done
# rpm suse package
echo -e "start packaging suse package"
pushd /workspace
case ${{ matrix.job.arch }} in
aarch64)
sed -i "s/linux\/x64/linux\/arm64/g" ./res/rpm-flutter-suse.spec
;;
esac
HBB=`pwd` rpmbuild ./res/rpm-flutter-suse.spec -bb
pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }}
for name in rustdesk*??.rpm; do
mv "$name" /workspace/"${name%%.rpm}-suse.rpm"
done
- name: Publish debian/rpm package
if: env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
rustdesk-*.deb
rustdesk-*.rpm
- name: Upload deb
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: env.UPLOAD_ARTIFACT == 'true'
with:
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
path: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
# only x86_64 for arch since we can not find newest arm64 docker image to build
# old arch image does not make sense for arch since it is "arch" which always update to date
# and failed to makepkg arm64 on x86_64
- name: Patch archlinux PKGBUILD
if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true'
run: |
sed -i "s/x86_64/${{ matrix.job.arch }}/g" res/PKGBUILD
if [[ "${{ matrix.job.arch }}" == "aarch64" ]]; then
sed -i "s/x86_64/aarch64/g" ./res/PKGBUILD
fi
- name: Build archlinux package
if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true'
uses: rustdesk-org/arch-makepkg-action@04200739ed1d0bf6f2188b6736b26a767c57a7f9 # no release tag; commit 2026-05-26
with:
packages:
scripts: |
cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f
- name: Publish archlinux package
if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
res/rustdesk-${{ env.VERSION }}*.zst
# Same build as build-rustdesk-linux x86_64 -- same vcpkg/ffmpeg, same ubuntu18.04 container, same
# rust and flutter -- only with the drm feature on, so it ships as the separate
# rustdesk-unattended-wayland deb. libdrmtap is built on the runner because bionic's meson is too
# old for it. A separate job rather than a matrix entry of build-rustdesk-linux: appimage and
# flatpak need that job, and a failure here must not skip them.
build-rustdesk-linux-drm:
needs: [generate-bridge]
name: build rustdesk linux drm x86_64
runs-on: ubuntu-22.04
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Maximize build space
run: |
sudo rm -rf /opt/ghc
sudo rm -rf /usr/local/lib/android
sudo rm -rf /usr/share/dotnet
sudo apt-get update -y
sudo apt-get install -y nasm
sudo apt-get install -y qemu-user-static
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
submodules: recursive
- name: Set Swap Space
uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c # v1.0
with:
swap-size-gb: 12
- name: Free Space
run: |
df -h
free -m
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: ${{ env.RUST_VERSION }}
targets: x86_64-unknown-linux-gnu
components: "rustfmt"
- name: Save Rust toolchain version
run: |
RUST_TOOLCHAIN_VERSION=$(cargo --version | awk '{print $2}')
echo "RUST_TOOLCHAIN_VERSION=$RUST_TOOLCHAIN_VERSION" >> $GITHUB_ENV
- name: Disable rust bridge build
run: |
# only build cdylib
sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml
- name: Restore bridge files
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: bridge-artifact
path: ./
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
with:
vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
doNotCache: false
- name: Install vcpkg dependencies
run: |
sudo apt install -y libva-dev && apt show libva-dev
if ! $VCPKG_ROOT/vcpkg \
install \
--triplet x64-linux \
--x-install-root="$VCPKG_ROOT/installed"; then
find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
echo "$_1:"
echo "======"
cat "$_1"
echo "======"
echo ""
done
exit 1
fi
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-x64-linux-rel-out.log" || true
shell: bash
# The container's meson is too old to build libdrmtap, so build it here from the pin in
# build.py and hand the .so to the container below via DRMTAP_PREBUILT_DIR.
- name: Build libdrmtap
run: |
sudo apt-get install -y meson ninja-build pkg-config \
libdrm-dev libegl1-mesa-dev libgles2-mesa-dev
python3 - <<'PY'
import importlib.util, sys
spec = importlib.util.spec_from_file_location("b", "build.py")
b = importlib.util.module_from_spec(spec)
sys.argv = ["build.py"]
spec.loader.exec_module(b)
print(f"::notice::built {b.build_libdrmtap_so()}")
PY
shell: bash
- uses: rustdesk-org/run-on-arch-action@d3fcfbb632b84cf7f6bc772bfaaa2c2f4f8789a8 # no release tag; commit 2026-05-26
name: Build rustdesk
id: vcpkg
with:
arch: x86_64
distro: ubuntu18.04
githubToken: ${{ github.token }}
setup: |
ls -l "${PWD}"
ls -l /opt/artifacts/vcpkg/installed
dockerRunArgs: |
--volume "${PWD}:/workspace"
--volume "/opt/artifacts:/opt/artifacts"
shell: /bin/bash
install: |
apt-get update -y
echo -e "installing deps"
apt-get install -y \
build-essential \
clang \
cmake \
curl \
gcc \
git \
g++ \
libayatana-appindicator3-dev \
libasound2-dev \
libclang-10-dev \
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \
libpulse-dev \
libva-dev \
libxcb-randr0-dev \
libxcb-shape0-dev \
libxcb-xfixes0-dev \
libxdo-dev \
libxfixes-dev \
llvm-10-dev \
nasm \
ninja-build \
pkg-config \
tree \
python3 \
rpm \
unzip \
wget \
xz-utils \
libssl-dev
# we have libopus compiled by us.
apt-get remove -y libopus-dev || true
# output devs
ls -l ./
tree -L 3 /opt/artifacts/vcpkg/installed
run: |
# disable git safe.directory
git config --global --add safe.directory "*"
# rust
pushd /opt
# do not use rustup, because memory overflow in qemu
wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-${{env.RUST_TOOLCHAIN_VERSION}}-x86_64-unknown-linux-gnu.tar.gz
tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz
cd rust-${{env.RUST_TOOLCHAIN_VERSION}}-x86_64-unknown-linux-gnu && ./install.sh
rm -rf rust-${{env.RUST_TOOLCHAIN_VERSION}}-x86_64-unknown-linux-gnu
# edit config
mkdir -p ~/.cargo/
echo """
[source.crates-io]
registry = 'https://github.com/rust-lang/crates.io-index'
""" > ~/.cargo/config
cat ~/.cargo/config
# start build
pushd /workspace
export VCPKG_ROOT=/opt/artifacts/vcpkg
# use the .so built on the runner; build.py checks it is the pinned checkout
export DRMTAP_PREBUILT_DIR=/workspace/third_party/libdrmtap/build-pkg
# ask build.py for the features so this line and the packaging line cannot drift
FEATURES=$(python3 ./build.py --flutter --drm --hwcodec --unix-file-copy-paste --print-features)
# an empty or error-shaped value would silently build a stock binary
for want in drm drm-wake; do
case ",$FEATURES," in
*",$want,"*) ;;
*) echo "::error::build.py returned no '$want' feature: $FEATURES"; exit 1 ;;
esac
done
# Run the drm-gated unit tests HERE, because nowhere else does. The ordinary linux CI
# runs the suite, but `drm` is opt-in, so every test behind that feature -- the cursor
# rotation and hotspot ones especially -- has no continuous coverage at all otherwise.
# Debug rather than --release: these are pure logic tests and the release build is
# already the slow part of this job.
# A MINIMAL feature set, not the production $FEATURES: these are pure logic tests of
# the drm paths, and pulling hwcodec in makes the test binary fail to link at all
# (undefined reference to fcntl64 in this container), so the tests never run.
#
# Two packages, because `cargo test --lib` at the root builds the ROOT package only:
# the provenance tests live in libs/scrap and would never be selected from here. The
# ordinary linux CI runs --workspace but without the drm feature, so these are not
# covered there either.
#
# awk sums the counts rather than bc, which this container does not install.
#
# A cargo test FILTER that matches nothing exits 0, so a rename would turn this into a
# green no-op. Assert each command actually ran something.
run_drm_tests() {
local desc="$1"; shift
local out
out=$("$@" 2>&1) || { echo "$out"; echo "::error::drm tests failed: $desc"; exit 1; }
local ran
ran=$(echo "$out" | awk '/^running [0-9]+ tests?$/ { n += $2 } END { print n+0 }')
echo "$desc ran $ran tests"
if [ "$ran" -lt 1 ]; then
echo "::error::$desc selected no tests; it was renamed or removed"
exit 1
fi
}
run_drm_tests "rustdesk drm_capturer_tests" \
cargo test --locked --lib --features drm drm_capturer_tests
run_drm_tests "rustdesk ipc::test" \
cargo test --locked --lib --features drm ipc::test
run_drm_tests "scrap hotspot_provenance_tests" \
cargo test --locked -p scrap --features drm hotspot_provenance_tests
run_drm_tests "scrap hotspot_guess_tests" \
cargo test --locked -p scrap --features drm hotspot_guess_tests
cargo build --locked --lib --features "$FEATURES" --release
rm -rf target/release/deps target/release/build
rm -rf ~/.cargo
# Setup Flutter
# disable git safe.directory
git config --global --add safe.directory "*"
export PATH=/opt/flutter/bin:$PATH
pushd /opt
wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz
tar xf flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz
flutter doctor -v
if [[ "3.24.5" == ${{ env.FLUTTER_VERSION }} ]]; then
pushd /opt/flutter
git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
popd
fi
# build flutter
pushd /workspace
export CARGO_INCREMENTAL=0
export DEB_ARCH=amd64
python3 ./build.py --flutter --drm --hwcodec --unix-file-copy-paste --skip-cargo
for name in rustdesk*??.deb; do
mv "$name" "${name%%.deb}-x86_64.deb"
done
# build.py can exit 0 on some inner failures, so check the artifact rather than the status.
# The package name is the informed consent for consent-free capture, so a stock binary must
# never ship under it: assert the bundled library AND the dlopen path in the binary.
- name: Check the deb is a drm build
run: |
set -euo pipefail
# Resolve by glob, not from env.VERSION: build.py names the deb from Cargo.toml, so a
# hardcoded name fails with a bare exit 1 the first time those two drift.
shopt -s nullglob
debs=(rustdesk-unattended-wayland-*-x86_64.deb)
if [ "${#debs[@]}" -ne 1 ]; then
echo "::error::expected one rustdesk-unattended-wayland-*-x86_64.deb, found ${#debs[@]}: ${debs[*]-none}"
exit 1
fi
deb="${debs[0]}"
echo "DRM_DEB=$deb" >> "$GITHUB_ENV"
contents="$(dpkg -c "$deb")"
if [[ ! "$contents" =~ usr/lib/rustdesk/libdrmtap\.so\.0\.[0-9]+\.[0-9]+ ]]; then
echo "::error::$deb has no versioned libdrmtap.so.0.x.y"
exit 1
fi
if [[ "$contents" != *"usr/lib/rustdesk/libdrmtap.so.0 ->"* ]]; then
echo "::error::$deb has no libdrmtap.so.0 soname symlink"
exit 1
fi
rm -rf /tmp/deb && dpkg-deb -R "$deb" /tmp/deb
if ! grep -qF /usr/lib/rustdesk/libdrmtap.so.0 /tmp/deb/usr/share/rustdesk/lib/librustdesk.so; then
echo "::error::$deb was not built with the drm feature"
exit 1
fi
shell: bash
- name: Publish debian package
if: env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
${{ env.DRM_DEB }}
# No UPLOAD_ARTIFACT gate: on a PR this is the only way to get at the deb that was just built.
# always(), because a deb that failed the check above is the one most worth downloading.
- name: Upload deb
if: always() && env.DRM_DEB != ''
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ env.DRM_DEB }}
path: ${{ env.DRM_DEB }}
build-rustdesk-linux-sciter:
if: ${{ inputs.upload-artifact }}
runs-on: ${{ matrix.job.on }}
name: build-rustdesk-linux-sciter ${{ matrix.job.target }}
strategy:
fail-fast: false
matrix:
# use a high level qemu-user-static
job:
- {
arch: x86_64,
target: x86_64-unknown-linux-gnu,
on: ubuntu-22.04,
distro: ubuntu18.04,
deb_arch: amd64,
sciter_arch: x64,
vcpkg-triplet: x64-linux,
extra_features: ",hwcodec,unix-file-copy-paste",
}
- {
arch: armv7,
target: armv7-unknown-linux-gnueabihf,
on: ubuntu-22.04-arm,
distro: ubuntu18.04-rustdesk,
deb_arch: armhf,
sciter_arch: arm32,
vcpkg-triplet: arm-linux,
extra_features: ",unix-file-copy-paste",
}
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
submodules: recursive
- name: Modify vcpkg.json for armv7
if: matrix.job.vcpkg-triplet == 'arm-linux'
run: |
# Replace the baseline in vcpkg.json with ARMV7_VCPKG_COMMIT_ID for armv7 builds
sed -i 's/"baseline": ".*"/"baseline": "${{ env.ARMV7_VCPKG_COMMIT_ID }}"/' vcpkg.json
echo "Modified vcpkg.json for armv7 build:"
grep -A 2 -B 2 '"baseline"' vcpkg.json
- name: Set Swap Space
if: matrix.job.arch == 'armv7'
uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c # v1.0
with:
swap-size-gb: 12
- name: Free Space
run: |
df -h
free -m
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: ${{ env.SCITER_RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
- name: Save Rust toolchain version
run: |
RUST_TOOLCHAIN_VERSION=$(cargo --version | awk '{print $2}')
echo "RUST_TOOLCHAIN_VERSION=$RUST_TOOLCHAIN_VERSION" >> $GITHUB_ENV
- uses: rustdesk-org/run-on-arch-action@d3fcfbb632b84cf7f6bc772bfaaa2c2f4f8789a8 # no release tag; commit 2026-05-26
name: Build rustdesk sciter binary for ${{ matrix.job.arch }}
id: vcpkg
with:
arch: ${{ matrix.job.arch }}
distro: ${{ matrix.job.distro }}
githubToken: ${{ github.token }}
setup: |
ls -l "${PWD}"
dockerRunArgs: |
--volume "${PWD}:/workspace"
shell: /bin/bash
install: |
apt-get update
apt-get install -y \
build-essential \
clang \
curl \
gcc \
git \
g++ \
libayatana-appindicator3-dev \
libasound2-dev \
libclang-dev \
libdbus-1-dev \
libglib2.0-dev \
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \
liblzma-dev \
libpulse-dev \
libva-dev \
libxcb-randr0-dev \
libxcb-shape0-dev \
libxcb-xfixes0-dev \
libxdo-dev \
libxfixes-dev \
ninja-build \
pkg-config \
python3 \
python3.7 \
rpm \
unzip \
wget \
xz-utils \
zip \
libssl-dev
# arm-linux needs CMake and vcokg built from source as there
# are no prebuilts available from Kitware and Microsoft
if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then
# install gcc/g++ 8 for vcpkg and OpenSSL headers for CMake
apt-get install -y gcc-8 g++-8
# bootstrap CMake amd add it to PATH
git clone --depth 1 https://github.com/kitware/cmake -b "v${{ env.SCITER_ARMV7_CMAKE_VERSION }}" /tmp/cmake
pushd /tmp/cmake
./bootstrap --generator='Unix Makefiles' "--prefix=/opt/cmake-${{ env.SCITER_ARMV7_CMAKE_VERSION }}-linux-armhf"
make -j1 install
popd
rm -rf /tmp/cmake
export PATH="/opt/cmake-${{ env.SCITER_ARMV7_CMAKE_VERSION }}-linux-armhf/bin:$PATH"
fi
# bootstrap vcpkg and set VCPKG_ROOT
export VCPKG_ROOT=/opt/artifacts/vcpkg
mkdir -p /opt/artifacts
pushd /opt/artifacts
rm -rf vcpkg
git clone https://github.com/microsoft/vcpkg
pushd vcpkg
# build vcpkg helper executable with gcc-8 for arm-linux but use prebuilt one on x64-linux
if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then
git reset --hard ${{ env.ARMV7_VCPKG_COMMIT_ID }}
CC=/usr/bin/gcc-8 CXX=/usr/bin/g++-8 sh bootstrap-vcpkg.sh -disableMetrics
else
git reset --hard ${{ env.VCPKG_COMMIT_ID }}
sh bootstrap-vcpkg.sh -disableMetrics
fi
popd
popd
# rust
pushd /opt
# do not use rustup, because memory overflow in qemu
wget --output-document rust.tar.gz https://static.rust-lang.org/dist/rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}.tar.gz
tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz
pushd rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}
./install.sh
popd
rm -rf rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}
popd
# install newer nasm for aom
wget --output-document nasm.deb "http://ftp.us.debian.org/debian/pool/main/n/nasm/nasm_${{ env.SCITER_NASM_DEBVERSION }}_${{ matrix.job.deb_arch }}.deb"
dpkg -i nasm.deb
rm -f nasm.deb
run: |
# disable git safe.directory
git config --global --add safe.directory "*"
# set python3.7 as default python3
update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.7 1
# add built CMake to PATH and set VCPKG_FORCE_SYSTEM_BINARIES Afor arm-linux
if [ "${{ matrix.job.vcpkg-triplet }}" = "arm-linux" ]; then
export PATH="/opt/cmake-${{ env.SCITER_ARMV7_CMAKE_VERSION }}-linux-armhf/bin:$PATH"
export VCPKG_FORCE_SYSTEM_BINARIES=1
fi
# edit cargo config
mkdir -p ~/.cargo/
echo """
[source.crates-io]
registry = 'sparse+https://index.crates.io/'
""" > ~/.cargo/config
cat ~/.cargo/config
# install dependencies from vcpkg
export VCPKG_ROOT=/opt/artifacts/vcpkg
# remove this when support higher version
export USE_AOM_391=1
if ! $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"; then
find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
echo "$_1:"
echo "======"
cat "$_1"
echo "======"
echo ""
done
exit 1
fi
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true
# build rustdesk
python3 ./res/inline-sciter.py
export CARGO_INCREMENTAL=0
# armv7 is the only 32-bit target in this job that links the whole binary, and the
# release profile uses fat LTO with codegen-units=1. LLVM then merges every module
# into a single unit and runs past the ~3GB address space a 32-bit process gets,
# aborting rustc with "Rust cannot catch foreign exceptions" (a C++ bad_alloc from
# LLVM unwinding into rustc's Rust frames). Thin LTO keeps peak memory bounded and
# still allows cross-crate inlining; 64-bit targets keep fat LTO untouched.
if [ "${{ matrix.job.arch }}" = "armv7" ]; then
export CARGO_PROFILE_RELEASE_LTO=thin
export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16
fi
cargo build --locked --features inline${{ matrix.job.extra_features }} --release --bins --jobs 1
# make debian package
mkdir -p ./Release
mv ./target/release/rustdesk ./Release/rustdesk
wget -O ./Release/libsciter-gtk.so https://github.com/c-smile/sciter-sdk/raw/master/bin.lnx/${{ matrix.job.sciter_arch }}/libsciter-gtk.so
export DEB_ARCH=${{ matrix.job.deb_arch }}
./build.py --package ./Release
- name: Rename rustdesk
shell: bash
run: |
for name in rustdesk*??.deb; do
# use cp to duplicate deb files to fit other packages.
cp "$name" "${name%%.deb}-${{ matrix.job.arch }}-sciter.deb"
done
- name: Publish debian package
if: env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
- name: Upload deb
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: env.UPLOAD_ARTIFACT == 'true'
with:
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
path: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
build-appimage:
name: Build appimage ${{ matrix.job.target }}
needs: [build-rustdesk-linux]
runs-on: ubuntu-22.04
if: ${{ inputs.upload-artifact }}
strategy:
fail-fast: false
matrix:
job:
- { target: x86_64-unknown-linux-gnu, arch: x86_64 }
- { target: aarch64-unknown-linux-gnu, arch: aarch64 }
steps:
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
submodules: recursive
- name: Download Binary
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
path: .
- name: Rename Binary
run: |
mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb appimage/rustdesk.deb
- name: Build appimage package
shell: bash
run: |
# install libarchive-tools for bsdtar command used in AppImageBuilder.yml
sudo apt-get update -y
# https://github.com/AppImage/AppImageKit/wiki/FUSE
sudo apt-get install -y libarchive-tools libfuse2
# set-up appimage-builder
# https://github.com/AppImage/AppImageKit/issues/1395
sudo pip3 install "setuptools_scm<10"
sudo pip3 install git+https://github.com/rustdesk-org/appimage-builder.git
# run appimage-builder
pushd appimage
./build-apprun.sh ${{ matrix.job.arch }}
sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml
cmp AppDir/AppRun appimage-build/AppRun/v2.0.0/AppRun-Release-${{ matrix.job.arch }}
- name: Publish appimage package
if: env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
./appimage/rustdesk-${{ env.VERSION }}-*.AppImage
build-flatpak:
name: Build flatpak ${{ matrix.job.target }}${{ matrix.job.suffix }}
needs:
- build-rustdesk-linux
- build-rustdesk-linux-sciter
runs-on: ${{ matrix.job.on }}
if: ${{ inputs.upload-artifact }}
strategy:
fail-fast: false
matrix:
job:
- {
target: x86_64-unknown-linux-gnu,
# https://github.com/ostreedev/ostree/commit/4bac96a8c817beda37448f9b8c662162bb619981
distro: ubuntu22.04,
on: ubuntu-22.04,
arch: x86_64,
suffix: "",
}
- {
target: x86_64-unknown-linux-gnu,
distro: ubuntu22.04,
on: ubuntu-22.04,
arch: x86_64,
suffix: "-sciter",
}
- {
target: aarch64-unknown-linux-gnu,
# try out newer flatpak since error of "error: Nothing matches org.freedesktop.Platform in remote flathub"
distro: ubuntu22.04,
on: ubuntu-22.04-arm,
arch: aarch64,
suffix: "",
}
steps:
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
submodules: recursive
- name: Download Binary
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.deb
path: .
- name: Rename Binary
run: |
mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.deb flatpak/rustdesk.deb
- uses: rustdesk-org/run-on-arch-action@d3fcfbb632b84cf7f6bc772bfaaa2c2f4f8789a8 # no release tag; commit 2026-05-26
name: Build rustdesk flatpak package for ${{ matrix.job.arch }}
id: flatpak
with:
arch: ${{ matrix.job.arch }}
distro: ${{ matrix.job.distro }}
githubToken: ${{ github.token }}
setup: |
ls -l "${PWD}"
dockerRunArgs: |
--volume "${PWD}:/workspace"
shell: /bin/bash
install: |
apt-get update -y
apt-get install -y git flatpak flatpak-builder
run: |
# disable git safe.directory
git config --global --add safe.directory "*"
pushd /workspace
# flatpak deps
flatpak --user remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
# package
pushd flatpak
git clone https://github.com/flathub/shared-modules.git --depth=1
flatpak-builder --user --install-deps-from=flathub -y --force-clean --repo=repo ./build ./rustdesk.json
flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.flatpak com.rustdesk.RustDesk
- name: Publish flatpak package
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.flatpak
build-rustdesk-web:
if: False
name: build-rustdesk-web
runs-on: ubuntu-22.04
permissions:
contents: read
strategy:
fail-fast: false
env:
RELEASE_NAME: web-basic
steps:
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
submodules: recursive
- name: Prepare env
run: |
sudo apt-get update -y
sudo apt-get install -y wget npm
- name: Install flutter
uses: subosito/flutter-action@2783a3f08e1baf891508463f8c6653c258246225 # v2.12.0; https://github.com/subosito/flutter-action/issues/277
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Patch flutter
shell: bash
run: |
cd $(dirname $(dirname $(which flutter)))
if [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]]; then
git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
fi
- name: Patch sources for Flutter 3.44 web
# No-op while the web stays on Flutter 3.24.5; makes this job work as-is
# once FLUTTER_VERSION moves to 3.44.x (qr_code_scanner + fonts, see script).
shell: bash
run: |
if [[ "${{ env.FLUTTER_VERSION }}" == 3.44.* ]]; then
bash .github/patches/apply_flutter_3.44_web_patches.sh
fi
# https://rustdesk.com/docs/en/dev/build/web/
- name: Build web
shell: bash
run: |
pushd flutter/web/js
npm install yarn -g
npm install typescript -g
npm install protoc -g
# Install protoc first, see: https://google.github.io/proto-lens/installing-protoc.html
npm install ts-proto
# Only works with vite <= 2.8, see: https://github.com/vitejs/vite/blob/main/docs/guide/build.md#chunking-strategy
npm install vite@2.8
yarn install && yarn build
popd
pushd flutter/web
wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/web_deps.tar.gz
tar xzf web_deps.tar.gz
popd
pushd flutter
flutter build web --release
cd build
cp ../web/README.md web
# TODO: Remove the following line when the web is almost complete.
echo -e "\n\nThis build is for preview and not full functionality." >> web/README.md
dir_name="rustdesk-${{ env.VERSION }}-${{ env.RELEASE_NAME }}"
mv web "${dir_name}" && tar czf "${dir_name}".tar.gz "${dir_name}"
sha256sum "${dir_name}".tar.gz
popd
- name: Publish web
if: env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
flutter/build/rustdesk-${{ env.VERSION }}-${{ env.RELEASE_NAME }}.tar.gz