Compare commits

..

1 commit

Author SHA1 Message Date
github-actions[bot]
933b0d7c79 gui v1.1.7-beta.1 2026-06-15 01:00:29 +00:00
19 changed files with 592 additions and 133 deletions

View file

@ -16,7 +16,7 @@ jobs:
include:
- triple: x86_64-unknown-linux-gnu
on: ubuntu-22.04
bundles: appimage,appimage-updater
bundles: appimage,appimage-updater,deb,rpm
setup: |
sudo apt update && sudo apt install -y lld
ld.lld --version

View file

@ -8,7 +8,6 @@ The format is based on [Keep a Changelog].
## [Unreleased]
### Added
- Implement project sorting by creation date `#2941`
### Changed

195
Cargo.lock generated
View file

@ -112,6 +112,12 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "ar"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d67af77d68a931ecd5cbd8a3b5987d63a1d1d1278f7f6a60ae33db485cdebb69"
[[package]]
name = "arc-swap"
version = "1.9.1"
@ -764,7 +770,7 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692186b5ebe54007e45a59aea47ece9eb4108e141326c304cdc91699a7118a22"
dependencies = [
"nom",
"nom 7.1.3",
"proc-macro2",
"quote",
"syn 2.0.117",
@ -1349,6 +1355,17 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
[[package]]
name = "enum-display-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f16ef37b2a9b242295d61a154ee91ae884afff6b8b933b486b12481cc58310ca"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "enum-map"
version = "2.7.3"
@ -1369,6 +1386,17 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "enum-primitive-derive"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba7795da175654fe16979af73f81f26a8ea27638d8d9823d317016888a63dc4c"
dependencies = [
"num-traits",
"quote",
"syn 2.0.117",
]
[[package]]
name = "enumflags2"
version = "0.7.12"
@ -1511,6 +1539,7 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
"crc32fast",
"miniz_oxide",
"zlib-rs",
]
[[package]]
@ -2634,6 +2663,15 @@ dependencies = [
"serde_json",
]
[[package]]
name = "keccak"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
dependencies = [
"cpufeatures 0.2.17",
]
[[package]]
name = "keyboard-types"
version = "0.7.0"
@ -2983,6 +3021,15 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "nom"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
dependencies = [
"memchr",
]
[[package]]
name = "ntapi"
version = "0.4.3"
@ -2992,12 +3039,87 @@ dependencies = [
"winapi",
]
[[package]]
name = "num"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
dependencies = [
"num-bigint",
"num-complex",
"num-integer",
"num-iter",
"num-rational",
"num-traits",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
[[package]]
name = "num-conv"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
[[package]]
name = "num-derive"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -4286,6 +4408,40 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "rpm"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5f8c4a1267cbbad96d1e7bf080ca20c669df79f638ef4e6c5e05a38b5d32c96"
dependencies = [
"base64 0.22.1",
"bitflags 2.11.1",
"digest 0.10.7",
"enum-display-derive",
"enum-primitive-derive",
"flate2",
"hex",
"itertools",
"log",
"memchr",
"nom 8.0.0",
"num",
"num-derive",
"num-traits",
"rpm-version",
"sha1 0.10.6",
"sha2 0.10.9",
"sha3",
"thiserror 2.0.18",
"zeroize",
]
[[package]]
name = "rpm-version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9274efa3f2ce8fe60fd8ea005a49955f92ddd81643eb38084b8e2c6c507018f"
[[package]]
name = "rtoolbox"
version = "0.0.5"
@ -4766,6 +4922,17 @@ dependencies = [
"stable_deref_trait",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"digest 0.10.7",
]
[[package]]
name = "sha1"
version = "0.11.0"
@ -4799,6 +4966,16 @@ dependencies = [
"digest 0.11.2",
]
[[package]]
name = "sha3"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
dependencies = [
"digest 0.10.7",
"keccak",
]
[[package]]
name = "shlex"
version = "1.3.0"
@ -6152,9 +6329,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.23.3"
version = "1.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7"
checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7"
dependencies = [
"getrandom 0.4.2",
"js-sys",
@ -6203,7 +6380,7 @@ dependencies = [
[[package]]
name = "vrc-get-gui"
version = "1.1.7-beta.0"
version = "1.1.7-beta.1"
dependencies = [
"arc-swap",
"async-compression",
@ -6309,7 +6486,7 @@ dependencies = [
"serde_json",
"serde_path_to_error",
"serde_repr",
"sha1",
"sha1 0.11.0",
"sha2 0.11.0",
"tokio",
"tokio-util",
@ -7486,6 +7663,7 @@ name = "xtask"
version = "0.1.0"
dependencies = [
"anyhow",
"ar",
"base64 0.22.1",
"cargo_metadata 0.23.1",
"chrono",
@ -7497,6 +7675,7 @@ dependencies = [
"minisign",
"object 0.39.1",
"plist",
"rpm",
"serde",
"serde_json",
"tar",
@ -7681,6 +7860,12 @@ dependencies = [
"typed-path",
]
[[package]]
name = "zlib-rs"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513"
[[package]]
name = "zmij"
version = "1.0.21"

View file

@ -1,6 +1,6 @@
[package]
name = "vrc-get-gui"
version = "1.1.7-beta.0"
version = "1.1.7-beta.1"
description = "A fast open-source alternative of VRChat Creator Companion"
homepage.workspace = true

View file

@ -29,11 +29,7 @@ import {
} from "@/components/ui/tooltip";
import type { TauriProject } from "@/lib/bindings";
import { commands } from "@/lib/bindings";
import {
dateToString,
dayToString,
formatDateOffset,
} from "@/lib/dateToString";
import { dateToString, formatDateOffset } from "@/lib/dateToString";
import { openSingleDialog } from "@/lib/dialog";
import { tc } from "@/lib/i18n";
import { toastThrownError } from "@/lib/toast";
@ -49,7 +45,7 @@ export function ProjectGridItem({
const typeIconClass = "w-5 h-5";
const { projectTypeKind, displayType, isLegacy, createdAt, lastModified } =
const { projectTypeKind, displayType, isLegacy, lastModified } =
getProjectDisplayInfo(project);
const removed = !project.is_exists;
@ -169,44 +165,22 @@ export function ProjectGridItem({
</div>
</div>
<div className="flex flex-row gap-1">
<div className="text-xs text-muted-foreground">
{tc("general:created at")}:{" "}
<Tooltip>
<TooltipTrigger>
<time dateTime={createdAt.toISOString()}>
<time className="font-normal">
{dayToString(project.created_at)}
</time>
<div className="text-xs text-muted-foreground">
{tc("general:last modified")}:{" "}
<Tooltip>
<TooltipTrigger>
<time dateTime={lastModified.toISOString()}>
<time className="font-normal">
{formatDateOffset(project.last_modified)}
</time>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>
{dateToString(project.created_at)}
</TooltipContent>
</TooltipPortal>
</Tooltip>
</div>
<p className="text-xs text-muted-foreground">/</p>
<div className="text-xs text-muted-foreground">
{tc("general:last modified")}:{" "}
<Tooltip>
<TooltipTrigger>
<time dateTime={lastModified.toISOString()}>
<time className="font-normal">
{formatDateOffset(project.last_modified)}
</time>
</time>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>
{dateToString(project.last_modified)}
</TooltipContent>
</TooltipPortal>
</Tooltip>
</div>
</time>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>
{dateToString(project.last_modified)}
</TooltipContent>
</TooltipPortal>
</Tooltip>
</div>
<div className="mt-2 flex flex-wrap gap-2 justify-end compact:gap-1">

View file

@ -29,11 +29,7 @@ import {
import { assertNever } from "@/lib/assert-never";
import type { TauriProject, TauriProjectType } from "@/lib/bindings";
import { commands } from "@/lib/bindings";
import {
dateToString,
dayToString,
formatDateOffset,
} from "@/lib/dateToString";
import { dateToString, formatDateOffset } from "@/lib/dateToString";
import { type DialogContext, openSingleDialog, showDialog } from "@/lib/dialog";
import { tc, tt } from "@/lib/i18n";
import { router } from "@/lib/main";
@ -82,7 +78,7 @@ export function ProjectRow({
const noGrowCellClass = `${cellClass} w-1`;
const typeIconClass = "w-5 h-5";
const { projectTypeKind, displayType, isLegacy, createdAt, lastModified } =
const { projectTypeKind, displayType, isLegacy, lastModified } =
getProjectDisplayInfo(project);
const openProjectFolder = () =>
@ -109,7 +105,7 @@ export function ProjectRow({
<tr
className={`group even:bg-secondary/30 ${removed || loading || !(project.is_valid ?? true) ? "opacity-50" : ""}`}
>
<td className={noGrowCellClass}>
<td className={`${cellClass} w-3`}>
<div className={"relative flex"}>
<FavoriteStarToggleButton
favorite={project.favorite}
@ -151,7 +147,7 @@ export function ProjectRow({
</TooltipPortal>
</Tooltip>
</td>
<td className={noGrowCellClass}>
<td className={`${cellClass} w-[8em] min-w-[8em]`}>
<div className="flex flex-row gap-2">
<div className="flex items-center">
{projectTypeKind === "avatars" ? (
@ -175,22 +171,6 @@ export function ProjectRow({
<td className={noGrowCellClass}>
<p className="font-normal">{project.unity}</p>
</td>
<td className={noGrowCellClass}>
<Tooltip>
<TooltipTrigger>
<time dateTime={createdAt.toISOString()}>
<time className="font-normal">
{dayToString(project.created_at)}
</time>
</time>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>
{dateToString(project.created_at)}
</TooltipContent>
</TooltipPortal>
</Tooltip>
</td>
<td className={noGrowCellClass}>
<Tooltip>
<TooltipTrigger>
@ -521,14 +501,12 @@ export function getProjectDisplayInfo(project: TauriProject) {
const projectTypeKind = ProjectDisplayType[project.project_type] ?? "unknown";
const displayType = tc(`projects:type:${projectTypeKind}`);
const isLegacy = LegacyProjectTypes.includes(project.project_type);
const createdAt = new Date(project.created_at);
const lastModified = new Date(project.last_modified);
return {
projectTypeKind,
displayType,
isLegacy,
createdAt,
lastModified,
};
}

View file

@ -30,7 +30,6 @@ const sortingOptions: { key: SimpleSorting; label: string }[] = [
{ key: "name", label: "general:name" },
{ key: "type", label: "projects:type" },
{ key: "unity", label: "projects:unity" },
{ key: "createdAt", label: "general:created at" },
{ key: "lastModified", label: "general:last modified" },
];

View file

@ -11,13 +11,7 @@ import { toastThrownError } from "@/lib/toast";
import { compareUnityVersionString } from "@/lib/version";
import { ProjectRow } from "./-project-row";
export const sortings = [
"createdAt",
"lastModified",
"name",
"unity",
"type",
] as const;
export const sortings = ["lastModified", "name", "unity", "type"] as const;
type SimpleSorting = (typeof sortings)[number];
type Sorting = SimpleSorting | `${SimpleSorting}Reversed`;
@ -164,18 +158,6 @@ export function ProjectsTableCard({
</small>
</button>
</th>
<th className={`${thClass} ${headerBg("createdAt")}`}>
<button
type="button"
className={"flex w-full project-table-button"}
onClick={() => setSorting("createdAt")}
>
{icon("createdAt")}
<small className="font-normal leading-none">
{tc("general:created at")}
</small>
</button>
</th>
<th className={`${thClass} ${headerBg("lastModified")}`}>
<button
type="button"
@ -212,12 +194,6 @@ export function sortSearchProjects(
searched.sort((a, b) => b.last_modified - a.last_modified);
switch (sorting) {
case "createdAt":
searched.sort((a, b) => b.created_at - a.created_at);
break;
case "createdAtReversed":
searched.sort((a, b) => a.created_at - b.created_at);
break;
case "lastModified":
searched.sort((a, b) => b.last_modified - a.last_modified);
break;

View file

@ -1,5 +1,5 @@
Name: alcom
Version: 1.1.6
Version: 1.1.7~beta.1
Release: 1%{?dist}
Summary: A short description of my custom application

View file

@ -0,0 +1,10 @@
Package: alcom
Version: {{version}}-1
Architecture: {{arch}}
Installed-Size: {{estimated_size}}
Maintainer: anatawa12 <i@anatawa12.com>
Priority: optional
Homepage: https://vrc-get.anatawa12.com/alcom/
Depends: libwebkit2gtk-4.1-0, libgtk-3-0, libc6 (>= {{libc_version}}), libc6 (>= {{libgcc_version}})
Description: ALCOM - Alternative Creator Companion
ALCOM is a fast and open-source alternative VCC (VRChat Creator Companion) written in rust and tauri.

View file

@ -1,3 +1,9 @@
alcom (1.1.7~beta.1-1) stable;
* Upgraded version to 1.1.7-beta.1
-- anatawa12 <i@anatawa12.com> Mon, 15 Jun 2026 01:00:28 +0000
alcom (1.1.6-1) UNRELEASED; urgency=low
* No changelog are provided for this package

View file

@ -1,16 +1,6 @@
import type React from "react";
import { tc } from "@/lib/i18n";
export function dayToString(dateIn: Date | number | string) {
const date = typeof dateIn !== "object" ? new Date(dateIn) : dateIn;
const year = date.getFullYear().toString().padStart(4, "0");
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
return `${year}-${month}-${day}`;
}
export function dateToString(dateIn: Date | number | string) {
const date = typeof dateIn !== "object" ? new Date(dateIn) : dateIn;

View file

@ -37,8 +37,6 @@
"general:error:failed to create dir": "Failed to create directory (missing permission?): {{err}}",
"general:error:failed to create dir missing drive": "Failed to create directory. You may forget to connect the drive.",
"general:created at": "Added",
"general:last modified": "Last Modified",
"general:last modified:moments": "Moments ago",
"general:last modified:minutes_one": "{{count}} minute ago",

View file

@ -37,8 +37,6 @@
"general:error:failed to create dir": "ディレクトリの作成に失敗しました: {{err}}",
"general:error:failed to create dir missing drive": "ディレクトリの作成に失敗しました。保存先のドライブの接続を忘れているかもしれません。",
"general:created at": "追加日",
"general:last modified": "最終更新日",
"general:last modified:moments": "たった今",
"general:last modified:minutes": "{{count}}分前",

View file

@ -22,6 +22,8 @@ ureq = { version = "3.3.0", features = ["gzip", "native-tls"], default-features
flate2 = "1.1.1"
tar = { version = "0.4.46", features = [], default-features = false }
plist = "1.9.0"
rpm = { version = "0.24.0", default-features = false, features = ["gzip-compression", "payload"] }
ar = "0.9.0"
fs_extra = "1.3.0"
base64 = "0.22.1"
minisign = "0.9.1"

View file

@ -1,12 +1,14 @@
use crate::utils::{self, build_dir, build_target, target_os};
use anyhow::{Context, Result, bail};
use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
mod app;
mod appimage;
mod deb;
mod dmg;
mod linux;
mod rpm;
mod setup_exe;
/// Individual bundle artifact that can be produced.
@ -50,18 +52,16 @@ pub(crate) enum BundleKind {
///
/// Unlike dmg depends on app, deb/rpm doesn't depend on this bundle.
Buildroot,
/// Debian package
Deb,
/// RPM package
Rpm,
/// Windows setup.exe
SetupExe,
/// Windows setup.exe in zip (requires setup.exe to already exist in bundle dir)
SetupExeZip,
/// Windows setup.exe for updater
ExeUpdater,
// deleted
#[value(hide = true)]
Deb,
#[value(hide = true)]
Rpm,
}
/// Bundles the ALCOM application for the target platform.
@ -113,12 +113,6 @@ impl crate::Command for Command {
let bundles = self.bundles.as_slice();
if bundles.contains(&BundleKind::Deb) || bundles.contains(&BundleKind::Rpm) {
bail!(
"--bundles deb and --bundles rpm are removed. Please use native packaging configuration at vrc-get-gui/bundles"
)
}
if bundles.is_empty() {
println!("Note: no bundles are specified");
}
@ -147,6 +141,14 @@ impl crate::Command for Command {
linux::create_install_build_root(&ctx, self.buildroot.as_deref())?;
}
if bundles.contains(&BundleKind::Deb) {
deb::create_deb(&ctx)?;
}
if bundles.contains(&BundleKind::Rpm) {
rpm::create_rpm(&ctx)?;
}
if bundles.contains(&BundleKind::SetupExe) {
setup_exe::create_setup_exe(&ctx)?;
}
@ -212,6 +214,14 @@ impl<'a> BundleContext<'a> {
self.version.as_str()
}
pub fn short_description(&self) -> &str {
"ALCOM - Alternative Creator Companion"
}
pub fn long_description(&self) -> &str {
"ALCOM is a fast and open-source alternative VCC (VRChat Creator Companion) written in rust and tauri."
}
/// Binary name without extension (e.g. `ALCOM`).
pub fn binary_name(&self) -> &str {
"ALCOM"

View file

@ -0,0 +1,108 @@
use crate::bundle_alcom::BundleContext;
use crate::bundle_alcom::linux::*;
use crate::utils::tar::TarBuilderExt;
use crate::utils::{CountingIo, tar, target_arch};
use anyhow::{Context, Result, bail};
use flate2::Compression;
use flate2::write::GzEncoder;
use std::io::Write;
use std::{fs, io};
fn deb_arch(triple: &str) -> Result<&str> {
match target_arch(triple) {
"aarch64" => Ok("arm64"),
"x86_64" => Ok("amd64"),
_ => {
bail!(
"unsupported architecture in target triple for deb: {}",
triple
)
}
}
}
pub fn create_deb(ctx: &BundleContext<'_>) -> Result<()> {
let arch = deb_arch(ctx.target_tuple)?;
let pkg_name = format!("alcom_{}-1_{arch}", ctx.version());
let (estimated_size, data_tar_gz) = {
let gz = GzEncoder::new(Vec::new(), Compression::default());
let mut tar = tar::Builder::new(CountingIo::new(gz));
create_install_build_root_impl(ctx, &mut tar).context("creating data.tar.gz")?;
let finished_gz_count = tar.into_inner()?;
let estimated_size = finished_gz_count.count();
let finished_gz = finished_gz_count.into_inner();
let data_tar_gz = finished_gz.finish().context("finishing data.tar.gz")?;
(estimated_size, data_tar_gz)
};
let library = detect_library_versions(&ctx.binary_path())?;
// Build control.tar.gz
let control_tar_gz = {
let mut control_tar_gz = Vec::new();
let gz = GzEncoder::new(&mut control_tar_gz, Compression::best());
let mut tar = tar::Builder::new(gz);
let control = {
let template_path = ctx.gui_dir.join("bundle/deb-control");
fs::read_to_string(&template_path)
.with_context(|| format!("reading {}", template_path.display()))?
.replace("{{version}}", ctx.version())
.replace("{{arch}}", arch)
.replace("{{estimated_size}}", &(estimated_size / 1024).to_string())
.replace("{{libc_version}}", &library.libc)
.replace("{{libgcc_version}}", &library.libgcc)
};
tar.append_file_data(0o644, "control", io::Cursor::new(control.as_bytes()))
.context("appending control file")?;
let gz = tar.into_inner().context("finishing control tar")?;
gz.finish().context("finishing control gzip")?;
control_tar_gz
};
// Assemble .deb as an ar archive.
let deb_dir = ctx.bundle_dir.join("deb");
fs::create_dir_all(&deb_dir)?;
let deb_name = format!("{pkg_name}.deb");
let deb_out = deb_dir.join(&deb_name);
{
let deb_file = fs::File::create(&deb_out)
.with_context(|| format!("creating {}", deb_out.display()))?;
let mut builder = ar::Builder::new(deb_file);
// debian-binary
let debian_binary = b"2.0\n";
let mut header = ar::Header::new(b"debian-binary".to_vec(), debian_binary.len() as u64);
header.set_mode(0o100644);
builder
.append(&header, &mut debian_binary.as_slice())
.context("appending debian-binary")?;
// control.tar.gz
let mut header = ar::Header::new(b"control.tar.gz".to_vec(), control_tar_gz.len() as u64);
header.set_mode(0o100644);
builder
.append(&header, &mut control_tar_gz.as_slice())
.context("appending control.tar.gz")?;
// data.tar.gz
let mut header = ar::Header::new(b"data.tar.gz".to_vec(), data_tar_gz.len() as u64);
header.set_mode(0o100644);
builder
.append(&header, &mut data_tar_gz.as_slice())
.context("appending data.tar.gz")?;
builder.into_inner()?.flush()?;
}
println!("created: {}", deb_out.display());
Ok(())
}

View file

@ -1,5 +1,7 @@
use super::BundleContext;
use anyhow::{Context, Result};
use crate::utils::tar::TarBuilderExt;
use anyhow::{Context, Result, bail};
use std::collections::HashMap;
use std::path::Path;
use std::{fs, io};
@ -112,7 +114,6 @@ impl<'a> BuildRootFs for RealBuildRootFs<'a> {
}
fn create_file(&mut self, mode: u32, relative: &str, data: &mut dyn io::Read) -> Result<()> {
let _ = mode; // suppress warning on windows
let path = &self.0.join(relative);
std::io::copy(
data,
@ -131,6 +132,37 @@ impl<'a> BuildRootFs for RealBuildRootFs<'a> {
}
}
impl<W: io::Write> BuildRootFs for tar::Builder<W> {
fn create_dir(&mut self, path: &str) -> Result<()> {
self.append_directory(path)
}
fn create_file(&mut self, mode: u32, path: &str, data: &mut dyn io::Read) -> Result<()> {
self.append_file_data(mode, path, data)
}
}
impl BuildRootFs for rpm::PackageBuilder {
fn create_dir(&mut self, path: &str) -> Result<()> {
self.with_dir_entry(rpm::FileOptions::dir(format!("/{path}")))
.map(|_| ())
.with_context(|| format!("creating directory {}", path))
}
fn create_file(&mut self, mode: u32, path: &str, data: &mut dyn io::Read) -> Result<()> {
let mut contents = vec![];
data.read_to_end(&mut contents)
.with_context(|| format!("reading data for {}", path))?;
self.with_file_contents(
contents,
rpm::FileOptions::new(format!("/{path}")).permissions(mode as u16),
)
.map(|_| ())
.with_context(|| format!("creating file {}", path))
}
}
/// Render the desktop file template from `alcom.desktop`.
///
/// The template uses `{{key}}` placeholders as in the tauri bundler.
@ -145,3 +177,134 @@ pub fn render_desktop_file(ctx: &BundleContext<'_>, exec: &str) -> Result<String
pub static LINUX_ICON_RESOLUTIONS: &[&str] = &["32x32", "64x64", "128x128"];
pub static LINUX_ICON_NAME: &str = "alcom"; // keep in sync with alcom.desktop template
pub struct LibraryVersions {
pub libc: String,
pub libgcc: String,
}
pub fn detect_library_versions(path: &Path) -> Result<LibraryVersions> {
use object::read::elf::ElfFile64;
use object::{Endianness, Object, ObjectSymbol};
let binary = fs::read(path).context("Reading binary")?;
let elf = ElfFile64::<Endianness>::parse(&binary).context("failed to parse binary")?;
let Some(versions) = elf.elf_section_table().versions(elf.endian(), elf.data())? else {
bail!("no version table found");
};
let versions = elf
.dynamic_symbols()
.map(|s| versions.version_index(elf.endian(), s.index()))
.flat_map(|i| versions.version(i).transpose())
.collect::<Result<Vec<_>, _>>()?;
let mut by_lib = HashMap::new();
for version in versions.into_iter() {
let lib = version.name().split(|&x| x == b'_').next().unwrap();
let version_name = version.name();
let version_str = version_name
.split(|&x| x == b'_')
.nth(1)
.unwrap_or(version_name);
if !matches!(version_str.first(), Some(c) if c.is_ascii_digit()) {
continue;
}
let version = VersionNumber::try_from(version_name).with_context(|| {
format!(
"failed to parse symbol version '{}' in {}",
String::from_utf8_lossy(version_name),
path.display()
)
})?;
let existing = by_lib.entry(lib).or_insert(VersionNumber::MIN);
if *existing < version {
*existing = version;
}
}
//for (lib, version) in &by_lib {
// let lib = std::str::from_utf8(lib)?;
// println!("{lib}: {version}");
//}
return Ok(LibraryVersions {
libc: by_lib
.get(&b"GLIBC"[..])
.context("no numeric GLIBC version requirement found in dynamic symbols")?
.to_string(),
libgcc: by_lib
.get(&b"GCC"[..])
.context("no numeric GCC version requirement found in dynamic symbols")?
.to_string(),
});
struct VersionNumber(String, Vec<u32>);
impl VersionNumber {
pub const MIN: VersionNumber = VersionNumber(String::new(), Vec::new());
}
impl<'a> TryFrom<&'a [u8]> for VersionNumber {
type Error = anyhow::Error;
fn try_from(value: &'a [u8]) -> std::result::Result<Self, Self::Error> {
let value = if let Some(index) = value.iter().position(|&x| x == b'_') {
value.split_at(index + 1).1
} else {
value
};
let value = std::str::from_utf8(value)?;
let components = value
.split(['.', '_'])
.map(std::str::FromStr::from_str)
.collect::<Result<Vec<_>, _>>()?;
Ok(Self(value.to_string(), components))
}
}
impl Ord for VersionNumber {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.1.cmp(&other.1)
}
}
impl PartialEq for VersionNumber {
fn eq(&self, other: &Self) -> bool {
self.cmp(other).is_eq()
}
}
impl PartialOrd for VersionNumber {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Eq for VersionNumber {}
impl std::fmt::Display for VersionNumber {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if f.alternate() {
let mut iter = self.1.iter();
f.write_fmt(format_args!("{}", iter.next().unwrap()))?;
for x in iter {
f.write_fmt(format_args!(".{}", x))?;
}
Ok(())
} else {
f.write_str(&self.0)
}
}
}
impl std::fmt::Debug for VersionNumber {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
}
}
}

View file

@ -0,0 +1,63 @@
use super::BundleContext;
use crate::bundle_alcom::linux::*;
use anyhow::{Context, Result};
use rpm::Dependency;
use std::fs;
pub fn create_rpm(ctx: &BundleContext<'_>) -> Result<()> {
let arch = rpm_arch(ctx.target_tuple);
let rpm_name = format!("alcom-{}-1.{arch}.rpm", ctx.version());
let rpm_dir = ctx.bundle_dir.join("rpm");
fs::create_dir_all(&rpm_dir)?;
let rpm_out = rpm_dir.join(&rpm_name);
let library = detect_library_versions(&ctx.binary_path())?;
let mut builder = rpm::PackageBuilder::new(
"alcom",
// RPM doesn't support '-' in their version name.
// It's recommended to use '~' instead.
// https://docs.fedoraproject.org/en-US/packaging-guidelines/Versioning/#_handling_non_sorting_versions_with_tilde_dot_and_caret
&ctx.version().replace('-', "~"),
"MIT",
arch,
ctx.short_description(),
);
builder.release("1").description(ctx.long_description());
builder.requires(Dependency::any(format!(
"libgcc_s.so.1(GCC_{})(64bit)",
library.libgcc
)));
builder.requires(Dependency::any(format!(
"libc.so.6(GLIBC_{})(64bit)",
library.libc
)));
builder.requires(Dependency::any("libgtk-3.so.0()(64bit)"));
builder.requires(Dependency::any("libwebkit2gtk-4.1.so.0()(64bit)"));
// Binary.
create_install_build_root_impl(ctx, &mut builder).context("adding files to rpm")?;
let pkg = builder.build().context("building rpm package")?;
pkg.write_file(&rpm_out)
.with_context(|| format!("writing {}", rpm_out.display()))?;
println!("created: {}", rpm_out.display());
Ok(())
}
/// RPM architecture string.
fn rpm_arch(triple: &str) -> &str {
if triple.starts_with("aarch64") {
"aarch64"
} else if triple.starts_with("x86_64") {
"x86_64"
} else {
panic!(
"unsupported architecture in target triple for rpm: {}",
triple
)
}
}