Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions .github/workflows/ci-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,17 @@ jobs:
- name: Build Files.App.Storage (Uno desktop)
run: dotnet build src/Files.App.Storage/Files.App.Storage.csproj -c Release -f net10.0-desktop

# Informational until the Phase 2 error inventory (docs/linux-port/phase2-errors.md) reaches zero.
- name: Build Files.App (Uno desktop)
run: dotnet build src/Files.App/Files.App.csproj -c Release -f net10.0-desktop
# Blocking: Win32 compat mode is off (the default since P4-Z), so no Win32/Shell code and no CsWin32 assembly may be needed.
- name: Build Files.App (Uno desktop, Win32 compat off)
run: dotnet build src/Files.App/Files.App.csproj -c Release -f net10.0-desktop -p:FilesWin32Compat=false

- name: Verify the desktop build did not build CsWin32
shell: bash
run: |
if [ -d src/Files.App.CsWin32/bin ] || [ -d src/Files.App.CsWin32/obj ]; then
echo "::error::Files.App.CsWin32 was built for the desktop target; something still depends on Win32 compat mode."
exit 1
fi

- name: Build and test platform test projects (if any)
shell: bash
Expand Down
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
<WindowsSdkPackageVersion>10.0.26100.67-preview</WindowsSdkPackageVersion>
<WindowsTargetFramework>$(BaseTargetFramework)-windows$(TargetWindowsVersion)</WindowsTargetFramework>
<DesktopTargetFramework>$(BaseTargetFramework)-desktop</DesktopTargetFramework>
<!-- Linux bring-up: compile existing Win32 code against CsWin32 types until it is replaced by Files.Platform services. -->
<FilesWin32Compat Condition="'$(FilesWin32Compat)' == ''">true</FilesWin32Compat>
<!-- Opt-in (-p:FilesWin32Compat=true): compiles the leftover Win32 code on the desktop TFM against CsWin32 types, for inventory only. -->
<FilesWin32Compat Condition="'$(FilesWin32Compat)' == ''">false</FilesWin32Compat>
<LangVersion>preview</LangVersion>
<!-- Uno's nullability annotations differ from WinUI's; keep nullable diagnostics as warnings on the Linux desktop build for now. -->
<WarningsAsErrors Condition="!$(TargetFramework.EndsWith('-desktop'))">nullable</WarningsAsErrors>
Expand Down
9 changes: 6 additions & 3 deletions docs/linux-port/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ The owner decided to focus on Linux first. Windows support is revisited only aft
| 1 | Abstraction skeleton (`Files.Platform.*`, tests, DI) | serial, small |
| 2 | **Linux bring-up: make `Files.App` compile and launch on Uno `net10.0-desktop`.** Retarget Controls + App to Uno. Exclude Windows-only folders (`Compile Remove`). Replace Win32/WinRT calls with interface calls, backed by stub or simple Linux implementations. Fix compile errors folder by folder. **✅ Done (2026-10-05):** the app compiles, launches and runs on Linux; the compat-off criterion moved to Phase 4. | parallel by folder ownership |
| 3 | Real Linux backends per interface (enumeration, watcher, file ops, trash, launcher, thumbnails, drives, clipboard…), with unit tests | parallel, one agent per interface |
| 4 | Replace the legacy WinRT storage layer (`BaseStorageFile`/`Folder`) with OwlCore storables, then switch Win32 compat mode off (moved from Phase 2) | parallel work packages (see Phase 4 plan, Revision 3) |
| 4 | Replace the legacy WinRT storage layer (`BaseStorageFile`/`Folder`) with OwlCore storables, then switch Win32 compat mode off (moved from Phase 2). **✅ Done (2026-10-05, P4-Z):** compat mode is off by default and the desktop build has 0 errors, which also meets Phase 2's criterion | parallel work packages (see Phase 4 plan, Revision 3) |
| 5 | UI blockers and UX parity: L-DND, SpeedGraph, StickyHeader, title bar, previews, single instance | parallel |
| 6 | Desktop integration and packaging (FileManager1 D-Bus, `.desktop`, Flatpak/AppImage/AUR) | parallel |
| 7 | Hardening (performance, HiDPI, themes, localisation) | parallel |
Expand All @@ -53,7 +53,7 @@ The Phase 2 workstreams in §7.3 still define folder ownership. Their goal is no
not "Windows code wrapped without behaviour change".

### Phase 2 build decisions (2026-10-04)
- **Win32 compat mode is the default** (`FilesWin32Compat=true` in `Directory.Build.props`). Existing Win32/Shell code compiles on Linux against the CsWin32 assembly, which is built automatically for Platform=x64. It cannot run on Linux, so any call that is reached at runtime must be replaced by a `Files.Platform` service. Switching compat mode off was Phase 2's exit criterion; since 2026-10-05 it is Phase 4's (P4-Z), because it depends on removing the legacy storage layer.
- **Win32 compat mode is off by default since P4-Z (2026-10-05)** (`FilesWin32Compat=false` in `Directory.Build.props`). The desktop build no longer compiles Win32/Shell code or builds the CsWin32 assembly. `-p:FilesWin32Compat=true` still works as an opt-in inventory lens: it compiles the Windows-only folders against CsWin32 and defines `FILES_WIN32_COMPAT`. Nothing compiled that way runs on Linux. Linux CI builds with compat off and fails on any error. (Until P4-Z compat mode was the default. Switching it off was Phase 2's exit criterion and became Phase 4's on 2026-10-05.)
- **Nullable diagnostics are warnings on `-desktop`**, because Uno's annotations differ from WinUI's. Re-enable once the build is clean.
- Stubbed behaviour is tagged `// LINUX-TODO(<area>)`. Use `rg -n LINUX-TODO src` to find the backlog.

Expand Down Expand Up @@ -247,7 +247,8 @@ Each workstream: define its interface methods from real call sites, implement th
(`Files.Core.Storage`) + the Phase 2 interfaces. `ShellViewModel` first, then properties, search, archives (`Zip*`),
FTP, virtual folders.
- Kept on a single owner (or a strictly serialized queue), because almost everything touches `ShellViewModel`.
- **Exit:** `Utils/Storage/StorageItems/*` is no longer referenced by the Linux build.
- **Exit:** `Utils/Storage/StorageItems/*` is no longer referenced by the Linux build. *Superseded by the Revision 3 exit below, which is met. The legacy items
(`FtpStorage*`, `SystemStorage*`, `ZipStorageFolder.IsZipPath`, `BaseStorage*`) still compile on Linux behind `LINUX-TODO(storage)` and remain follow-up work.*

#### Phase 4 plan, Revision 3 (2026-10-05): drop the legacy storage layer, then switch Win32 compat off
The two are one project: the legacy items (`Zip*`, `NativeStorageFile`, `VirtualStorageItem`, `Shell*`, `SystemStorageFolder`) call
Expand Down Expand Up @@ -287,6 +288,8 @@ Add first, delete last; default build stays green. Contracts (P4-0, then frozen)
| P4-Z | 1–2 | Flip `FilesWin32Compat=false`, fix leftovers, delete `#if WINDOWS` IStorageItem members, CI job blocking | all | strong |

Order: P4-0 → wave 1a (K, A, B, C, H, I) → wave 1b (F, D, G, J) → E → Z. Critical path ≈ 10 working days.
**Status (2026-10-05):** all 14 packages are merged or in review; P4-E (#73) and P4-Z close the phase. Compat off by default, 0 errors, headless smoke
test passed (browse, copy/move, trash/restore, browse into a zip, Properties, search, live updates), and the Linux CI compat-off build is blocking.
Gate after every merge: normal desktop build passes; `-p:FilesWin32Compat=false` error count ratchets down vs
`docs/linux-port/compat-off-baseline.txt`; `rg --no-ignore 'BaseStorage|StorageItems\.|Windows\.Win32|PInvoke\.|Win32Helper\.'` on the WP's
files (outside `*.Windows.cs`) → 0. Exit: compat off by default, 0 errors, headless smoke run passes, CI compat-off job blocking.
Expand Down
3 changes: 0 additions & 3 deletions docs/linux-port/compat-off-baseline.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
# Compiler error keys of: dotnet build src/Files.App -f net10.0-desktop -p:FilesWin32Compat=false
# One "file: error CODE: message" per line. Regenerate with scripts/linux/compat-off-check.sh --update.
src/Files.App/Data/Items/ContextMenu.cs: error CS0246: The type or namespace name 'MENU_ITEM_TYPE' could not be found (are you missing a using directive or an assembly reference?)
src/Files.App/Data/Items/WidgetRecentItem.cs: error CS0246: The type or namespace name 'IShellItem' could not be found (are you missing a using directive or an assembly reference?)
src/Files.App/ViewModels/ShellViewModel.cs: error CS0234: The type or namespace name 'Win32PInvoke' does not exist in the namespace 'Files.App.Helpers' (are you missing an assembly reference?)
5 changes: 5 additions & 0 deletions docs/linux-port/phase2-errors.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Phase 2: Files.App on Uno (net10.0-desktop) build inventory

> **Status (2026-10-05, P4-Z): resolved.** Win32 compat mode is off by default, and `dotnet build src/Files.App -f net10.0-desktop` reports
> 0 errors without building CsWin32. The last iterations went 2 → 53 → 2 → 0 errors (declaration errors hid the body errors), plus one more
> round after `IStorageItemWithPath.Item` became Windows-only (10 → 0). `-p:FilesWin32Compat=true` remains as an opt-in lens and still
> builds. The inventory below is historical.

Branch `linux/p2-head`. Generated from `dotnet build src/Files.App -f net10.0-desktop` (Uno.Sdk 6.7.30, Uno.WinUI 6.7.135).

## How the build is organised
Expand Down
4 changes: 4 additions & 0 deletions src/Files.App/Actions/FileSystem/EmptyRecycleBinAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ await confirmationDialog.TryShowAsync() is ContentDialogResult.Primary)
{
var banner = StatusCenterHelper.AddCard_EmptyRecycleBin(ReturnResult.InProgress);

#if WINDOWS
bool result = await STATask.Run(StorageTrashBinService.EmptyTrashBin, App.Logger);
#else
bool result = await Task.Run(StorageTrashBinService.EmptyTrashBin);
#endif

StatusCenterViewModel.RemoveItem(banner);

Expand Down
4 changes: 4 additions & 0 deletions src/Files.App/Data/Items/ContextMenu.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Copyright (c) Files Community
// Licensed under the MIT License.

#if WINDOWS || FILES_WIN32_COMPAT
using Windows.Win32.UI.WindowsAndMessaging;
#endif

namespace Files.App.Data.Items
{
Expand Down Expand Up @@ -31,7 +33,9 @@ public class Win32ContextMenuItem
public int ID { get; set; } // Valid only in current menu to invoke item
public string? Label { get; set; }
public string? CommandString { get; set; }
#if WINDOWS || FILES_WIN32_COMPAT
public MENU_ITEM_TYPE Type { get; set; }
#endif
public List<Win32ContextMenuItem>? SubItems { get; set; }
}
}
6 changes: 5 additions & 1 deletion src/Files.App/Data/Items/WidgetRecentItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// SPDX-License-Identifier: MPL-2.0

using Microsoft.UI.Xaml.Media.Imaging;
#if WINDOWS || FILES_WIN32_COMPAT
using Windows.Win32.UI.Shell;
#endif

namespace Files.App.Data.Items
{
Expand Down Expand Up @@ -31,10 +33,12 @@ public BitmapImage? Icon
/// </summary>
public required DateTime LastModified { get; set; }

#if WINDOWS || FILES_WIN32_COMPAT
/// <summary>
/// Gets or initializes the Windows shell item of the recent item; unused on Linux.
/// Gets or initializes the Windows shell item of the recent item.
/// </summary>
public IShellItem? ShellItem { get; init; }
#endif

/// <summary>
/// Loads thumbnail icon of the recent item.
Expand Down
5 changes: 5 additions & 0 deletions src/Files.App/Data/Models/PinnedFoldersManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,17 @@ public async Task<LocationItem> CreateLocationItemFromPathAsync(string path)

private static string? GetLibraryDisplayName(string libraryPath)
{
#if !WINDOWS
// Windows libraries (.library-ms) don't exist on Linux
return null;
#else
using var storable = WindowsStorable.TryParse(libraryPath);
if (storable is null)
return null;

var displayName = storable.GetDisplayName(SIGDN.SIGDN_NORMALDISPLAY);
return string.IsNullOrEmpty(displayName) ? null : displayName;
#endif
}

private async Task LoadIconForLocationItemAsync(LocationItem locationItem, string path, bool isFolder = true)
Expand Down
2 changes: 2 additions & 0 deletions src/Files.App/Extensions/ShellNewEntryExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Files.App.Extensions
{
public static class ShellNewEntryExtensions
{
#if WINDOWS || FILES_WIN32_COMPAT
public static async Task<List<ShellNewEntry>> GetNewContextMenuEntries()
{
var shellEntryList = new List<ShellNewEntry>();
Expand All @@ -25,6 +26,7 @@ public static async Task<List<ShellNewEntry>> GetNewContextMenuEntries()
{
return await SafetyExtensions.IgnoreExceptions(() => ShellNewMenuHelper.GetNewContextMenuEntryForType(extension), App.Logger);
}
#endif

public static async Task<FilesystemResult<BaseStorageFile>> Create(this ShellNewEntry shellEntry, string filePath, IShellPage associatedInstance)
{
Expand Down
4 changes: 4 additions & 0 deletions src/Files.App/Files.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@
<ItemGroup Condition="'$(TargetFramework)' == '$(DesktopTargetFramework)' And '$(FilesWin32Compat)' != 'true'">
<Compile Remove="Helpers\Win32\**" />
<Compile Remove="Utils\Shell\**" />
<Compile Include="Utils\Shell\ShellHelpers.cs" />
<Compile Remove="Utils\Taskbar\**" />
<Compile Remove="Utils\Signatures\**" />
<Compile Remove="Services\Windows\**" />
Expand Down Expand Up @@ -291,6 +292,9 @@
the Windows TFM with Platform=x64, which works on Linux) so Win32-touching code gets past declaration binding and the compiler
reports the remaining, Uno-specific errors. Nothing here runs on Linux; replace Win32 usage with Files.Platform interfaces,
then drop this switch. Prerequisite: dotnet build src/Files.App.CsWin32 -p:EnableWindowsTargeting=true -p:Platform=x64 -->
<PropertyGroup Condition="'$(FilesWin32Compat)' == 'true' And '$(TargetFramework)' == '$(DesktopTargetFramework)'">
<DefineConstants>$(DefineConstants);FILES_WIN32_COMPAT</DefineConstants>
</PropertyGroup>
<ItemGroup Condition="'$(FilesWin32Compat)' == 'true' And '$(TargetFramework)' == '$(DesktopTargetFramework)'">
<Reference Include="Files.App.CsWin32">
<HintPath>..\Files.App.CsWin32\bin\x64\$(Configuration)\$(WindowsTargetFramework)\Files.App.CsWin32.dll</HintPath>
Expand Down
8 changes: 8 additions & 0 deletions src/Files.App/Helpers/Application/AppLifecycleHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,11 @@ public static IServiceProvider ConfigureHost(AppModel appModel)
#endif
.AddSingleton<IAppThemeModeService, AppThemeModeService>()
.AddSingleton<IDialogService, DialogService>()
#if WINDOWS
.AddSingleton<ICommonDialogService, CommonDialogService>()
#else
.AddSingleton<ICommonDialogService, Files.App.Services.Desktop.DesktopCommonDialogService>()
#endif
.AddSingleton<IImageService, ImagingService>()
.AddSingleton<IThreadingService, ThreadingService>()
.AddSingleton<ILocalizationService, LocalizationService>()
Expand Down Expand Up @@ -404,7 +408,11 @@ public static IServiceProvider ConfigureHost(AppModel appModel)
#endif
.AddSingleton<IRemovableDrivesService, RemovableDrivesService>()
.AddSingleton<INetworkService, NetworkService>()
#if WINDOWS
.AddSingleton<IStartMenuService, StartMenuService>()
#else
.AddSingleton<IStartMenuService, Files.App.Services.Desktop.DesktopStartMenuService>()
#endif
.AddSingleton<IStorageCacheService, StorageCacheService>()
.AddSingleton<IIconCacheService, IconCacheService>()
.AddSingleton<IStorageArchiveService, StorageArchiveService>()
Expand Down
17 changes: 17 additions & 0 deletions src/Files.App/Helpers/Navigation/NavigationHelpers.Linux.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Files.Platform.Abstractions.Mime;
using Files.Platform.Linux.Launching;
using Files.Platform.Linux.Mime;
using Files.Shared.Helpers;
using Microsoft.Extensions.Logging;
using System.Globalization;
using System.IO;
Expand All @@ -33,6 +34,22 @@ private static async Task<bool> OpenPathLinuxAsync(string path, IShellPage assoc
return true;
}

// Members of an archive being browsed; the archive file itself still opens with its default application
if (FileExtensionHelpers.IsZipPath(path, includeRoot: false))
{
var resolved = await Ioc.Default.GetRequiredService<Files.Core.Storage.Contracts.IStorableResolver>().TryGetAsync(path);
if (resolved.Item is OwlCore.Storage.IFolder)
{
var foldersSettings = Ioc.Default.GetRequiredService<IUserSettingsService>().FoldersSettingsService;
await OpenPath(forceOpenInNewTab, foldersSettings.OpenFoldersInNewTab, path, associatedInstance, selectItems);
return true;
}

// LINUX-TODO(archives): open archive members by extracting them to a private temporary copy first
await DialogDisplayHelper.ShowDialogAsync(Strings.LinuxOpenFailedTitle.GetLocalizedResource(), Strings.LinuxOpenFailedText.GetLocalizedFormatResource(DisplaySanitizer.Field(path)));
return false;
}

if (!File.Exists(path))
{
await DialogDisplayHelper.ShowDialogAsync(Strings.FileNotFoundDialogTitle.GetLocalizedResource(), Strings.FileNotFoundDialogText.GetLocalizedResource());
Expand Down
31 changes: 31 additions & 0 deletions src/Files.App/Platforms/Desktop/Services/DesktopStubServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,35 @@ internal sealed class DesktopJumpListService : IWindowsJumpListService

public Task<IEnumerable<string>> GetFoldersAsync() => Task.FromResult<IEnumerable<string>>([]);
}

// LINUX-TODO(pickers): open file/folder choosers through the xdg-desktop-portal FileChooser; until then pickers report "cancelled"
internal sealed class DesktopCommonDialogService : ICommonDialogService
{
public bool Open_FileOpenDialog(nint hWnd, bool pickFoldersOnly, string[] filters, Environment.SpecialFolder defaultFolder, out string filePath, Guid? clientGuid = null)
{
filePath = string.Empty;
return false;
}

public bool Open_FileSaveDialog(nint hWnd, bool pickFoldersOnly, string[] filters, Environment.SpecialFolder defaultFolder, out string filePath)
{
filePath = string.Empty;
return false;
}

public bool Open_NetworkConnectionDialog(nint hWnd, bool hideRestoreConnectionCheckBox = false, bool persistConnectionAtLogon = false, bool readOnlyPath = false, string? remoteNetworkName = null, bool useMostRecentPath = false) => false;
}

// Linux has no Start Menu pins
internal sealed class DesktopStartMenuService : IStartMenuService
{
[Obsolete("Use IsPinnedAsync instead. This method is used for a workaround in ListedItem class to avoid major refactoring.")]
public bool IsPinned(string itemPath) => false;

public Task<bool> IsPinnedAsync(IStorable storable) => Task.FromResult(false);

public Task PinAsync(IStorable storable, string? displayName = null) => Task.CompletedTask;

public Task UnpinAsync(IStorable storable) => Task.CompletedTask;
}
}
Loading
Loading