diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml
index f34131ad..92566130 100644
--- a/.github/workflows/c-cpp.yml
+++ b/.github/workflows/c-cpp.yml
@@ -29,6 +29,8 @@ on:
env:
TERM: xterm
+ BTRFS_IMG: test/rmw-btrfs-test.img
+ BCACHEFS_IMG: test/rmw-bcachefs-test.img
jobs:
build:
@@ -180,14 +182,27 @@ jobs:
uses: actions/cache@v5
id: btrfs-img-cache
with:
- path: test/rmw-btrfs-test.img
+ path: ${{ env.BTRFS_IMG }}
key: ${{ hashFiles('test/rmw-btrfs-test.img.sha256sum') }}
- if: ${{ steps.btrfs-img-cache.outputs.cache-hit != 'true' }}
- run: curl -L -o test/rmw-btrfs-test.img 'https://www.dropbox.com/scl/fi/57g3ixd3w3tuz4qoc2zp1/rmw-btrfs-test.img?rlkey=yc7krtntswsa1bwz0sbugy4gi&st=hkgrht05&dl=0'
+ run: curl -L -o "$BTRFS_IMG" 'https://www.dropbox.com/scl/fi/57g3ixd3w3tuz4qoc2zp1/rmw-btrfs-test.img?rlkey=yc7krtntswsa1bwz0sbugy4gi&st=hkgrht05&dl=0'
- name: Test btrfs image existence
- run: test -f test/rmw-btrfs-test.img
+ run: test -f "$BTRFS_IMG"
+
+ - name: Cache bcachefs Image
+ uses: actions/cache@v5
+ id: bcachefs-img-cache
+ with:
+ path: ${{ env.BCACHEFS_IMG }}
+ key: ${{ hashFiles('test/rmw-bcachefs-test.img.sha256sum') }}
+
+ - if: ${{ steps.bcachefs-img-cache.outputs.cache-hit != 'true' }}
+ run: curl -L -o "$BCACHEFS_IMG" 'https://www.dropbox.com/scl/fi/key20cj08ca9engqt5kzi/rmw-bcachefs-test.img?rlkey=th6zw1ar7t0dy9m7qzah5wvq9&st=nrwczcqn&dl=0'
+
+ - name: Test bcachefs image existence
+ run: test -f "$BCACHEFS_IMG"
- name: Install Dependencies
run: |
diff --git a/.gitignore b/.gitignore
index 8db05af7..efe5d6c2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,4 @@ subprojects/**
!subprojects/**/*.wrap
/packaging/appimage/.env
/test/rmw-btrfs-test.img
+/test/rmw-bcachefs-test.img
diff --git a/ChangeLog b/ChangeLog
index 04b37db8..16ff2c66 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,4 +1,19 @@
+rmw (in-progress):
+ * BREAKING: The meson build option `want_btrfs_clone` has been renamed to
+ `want_ficlone`. Update any build scripts that pass `-Dwant_btrfs_clone=`
+ to use `-Dwant_ficlone=` instead.
+ * refactor: Rename btrfs.c/btrfs.h to ficlone.c/ficlone.h; rename
+ do_btrfs_clone() to do_ficlone() and is_btrfs() to is_ficlone_fs();
+ rename HAVE_LINUX_BTRFS macro to HAVE_FICLONE
+ * Add bcachefs test (test_bcachefs.sh); uses a pre-existing image and
+ skips if kernel bcachefs support or the image is absent
+ * docs: Add BTRFS AND BCACHEFS section to man page
+ * bugfix: Fix "Invalid cross-device link" error on bcachefs when source and
+ waste folder are on separate subvolumes (#526); use generic FICLONE ioctl
+ (linux/fs.h) instead of BTRFS_IOC_CLONE, and fall back to 'mv' when
+ rename() returns EXDEV on a same-device move
+
2026-04-07
- rmw 0.9.5:
diff --git a/README.md b/README.md
index 20151473..d955231e 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# rmw-0.9.5
+# rmw-0.10.0-dev
## Description
rmw (ReMove to Waste) is a trashcan/recycle bin utility for the command line.
diff --git a/man/rmw.1 b/man/rmw.1
index d9304afe..38ac1966 100644
--- a/man/rmw.1
+++ b/man/rmw.1
@@ -192,6 +192,17 @@ directory will not be used for the current run of rmw.
With the media mounted, once you manually create the waste directory
for that device (e.g. "/mnt/flash/.Trash-$UID") and run rmw, it will
automatically create the two required child directories "files" and "info".
+.SS BTRFS AND BCACHEFS
+rmw supports moving files across subvolumes on btrfs and bcachefs
+filesystems. Because the kernel treats subvolumes as separate
+filesystems, a cross-subvolume move would normally fail; rmw detects
+this case and performs a copy-then-delete instead, preserving the
+expected trash semantics.
+
+To use this feature, define a WASTE directory on the destination
+subvolume in your configuration file. For example, if your waste
+directory is on a different subvolume than the files you want to
+remove, rmw will handle the move transparently.
.SH EXAMPLES
.SS RESTORING
rmw -z ~/.local/share/Waste/files/foo
diff --git a/meson.build b/meson.build
index 9180b842..6267c18e 100644
--- a/meson.build
+++ b/meson.build
@@ -1,7 +1,7 @@
project(
'rmw',
'c',
- version: '0.9.5',
+ version: '0.10.0-dev',
meson_version: '>= 0.59.0',
default_options: [
'c_std=gnu99',
diff --git a/meson_options.txt b/meson_options.txt
index db9047ae..22305d0e 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -1,10 +1,10 @@
option('build_tests', type : 'boolean', value : true,
description : 'build tests')
option(
- 'want_btrfs_clone',
+ 'want_ficlone',
type : 'boolean',
value: 'true',
- description: 'Include support for cloning files between btrfs root volumes and subvolumes')
+ description: 'Include support for cloning files between subvolumes using FICLONE (btrfs, bcachefs, xfs, etc.)')
option('nls', type : 'boolean', value : true,
description : 'include native language support (install translations)')
diff --git a/src/config_rmw.c b/src/config_rmw.c
index 06798f7a..9081f6fe 100644
--- a/src/config_rmw.c
+++ b/src/config_rmw.c
@@ -20,7 +20,7 @@ along with this program. If not, see .
#include
-#include "btrfs.h"
+#include "ficlone.h"
#include "config_rmw.h"
#include "utils.h"
#include "main.h"
@@ -275,7 +275,7 @@ parse_line_waste(st_waste *waste_curr, struct Canfigger *node,
else if (p_state == -1)
exit(p_state);
- waste_curr->is_btrfs = is_btrfs(waste_curr->parent);
+ waste_curr->is_ficlone_fs = is_ficlone_fs(waste_curr->parent);
// get device number to use later for rename
struct stat st, mp_st;
diff --git a/src/btrfs.c b/src/ficlone.c
similarity index 90%
rename from src/btrfs.c
rename to src/ficlone.c
index 6312c3e8..38b4021a 100644
--- a/src/btrfs.c
+++ b/src/ficlone.c
@@ -23,22 +23,22 @@ along with this program. If not, see .
#include "globals.h"
#endif
-#ifdef HAVE_LINUX_BTRFS
+#ifdef HAVE_FICLONE
#include
-#include
+#include
#include
#include
#include
#include
#endif
-#include "btrfs.h"
+#include "ficlone.h"
#include "messages.h"
bool
-is_btrfs(const char *path)
+is_ficlone_fs(const char *path)
{
-#ifdef HAVE_LINUX_BTRFS
+#ifdef HAVE_FICLONE
struct statfs buf;
if (statfs(path, &buf) == -1)
@@ -57,9 +57,9 @@ is_btrfs(const char *path)
int
-do_btrfs_clone(const char *source, const char *dest, int *save_errno)
+do_ficlone(const char *source, const char *dest, int *save_errno)
{
-#ifdef HAVE_LINUX_BTRFS
+#ifdef HAVE_FICLONE
int src_fd, dest_fd;
struct stat src_stat;
@@ -91,7 +91,7 @@ do_btrfs_clone(const char *source, const char *dest, int *save_errno)
return dest_fd;
}
- int res = ioctl(dest_fd, BTRFS_IOC_CLONE, src_fd);
+ int res = ioctl(dest_fd, FICLONE, src_fd);
*save_errno = errno;
close(src_fd);
diff --git a/src/btrfs.h b/src/ficlone.h
similarity index 85%
rename from src/btrfs.h
rename to src/ficlone.h
index 9da4430f..27f1bdf0 100644
--- a/src/btrfs.h
+++ b/src/ficlone.h
@@ -18,15 +18,15 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
-#ifndef _INC_BTRFS_H
-#define _INC_BTRFS_H
+#ifndef _INC_FICLONE_H
+#define _INC_FICLONE_H
#include
#define BTRFS_SUPER_MAGIC 0x9123683E
-bool is_btrfs(const char *path);
+bool is_ficlone_fs(const char *path);
-int do_btrfs_clone(const char *source, const char *dest, int *save_errno);
+int do_ficlone(const char *source, const char *dest, int *save_errno);
#endif
diff --git a/src/main.c b/src/main.c
index 0aa4da14..9a48fcf8 100644
--- a/src/main.c
+++ b/src/main.c
@@ -26,7 +26,7 @@ along with this program. If not, see .
#include "purging.h"
#include "strings_rmw.h"
#include "messages.h"
-#include "btrfs.h"
+#include "ficlone.h"
#include "trashinfo.h"
@@ -387,7 +387,7 @@ damage of 5000 hp. You feel satisfied.\n"));
while (waste_curr != NULL)
{
if (waste_curr->dev_num == st_target.dev_num ||
- (waste_curr->is_btrfs && is_btrfs(argv[file_arg])))
+ (waste_curr->is_ficlone_fs && is_ficlone_fs(argv[file_arg])))
{
char *tmp_str = join_paths(waste_curr->files, st_target.base_name);
// *st_target.waste_dest_name = '\0';
@@ -421,7 +421,7 @@ damage of 5000 hp. You feel satisfied.\n"));
if (!S_ISDIR(st_file_arg.st_mode))
{
/* attempt btrfs clone if not a directory */
- r_result = do_btrfs_clone(src, dst, &save_errno);
+ r_result = do_ficlone(src, dst, &save_errno);
errno = save_errno;
}
else
@@ -448,7 +448,13 @@ damage of 5000 hp. You feel satisfied.\n"));
{
/* same device: simple rename */
r_result = rename(src, dst);
- /* rename sets errno on failure */
+ if (r_result != 0 && errno == EXDEV)
+ {
+ /* rename failed with EXDEV even though st_dev matched (e.g.,
+ bcachefs cross-subvolume). Fall back to copy+delete. */
+ r_result = safe_mv_via_exec(src, dst, &save_errno);
+ errno = save_errno;
+ }
}
}
@@ -702,10 +708,10 @@ main(const int argc, char *const argv[])
printf("PATH_MAX = %d\n", PATH_MAX);
if (verbose > 0)
-#ifdef HAVE_LINUX_BTRFS
- puts("btrfs_clone support: true");
+#ifdef HAVE_FICLONE
+ puts("ficlone support: true");
#else
- puts("btrfs_clone support: false");
+ puts("ficlone support: false");
#endif
const st_loc *st_location = get_locations(cli_user_options.alt_config_file);
diff --git a/src/meson.build b/src/meson.build
index d9a5ce5d..8fe94b0b 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -23,7 +23,7 @@ endif
src = [
'globals.c',
- 'btrfs.c',
+ 'ficlone.c',
'restore.c',
'config_rmw.c',
'parse_cli_options.c',
@@ -35,26 +35,26 @@ src = [
'utils.c',
]
-# Used for btrfs functions
+# Used for FICLONE support
has_statfs = false
-has_btrfs_header = false
+has_linux_fs_header = false
if host_sys == 'linux'
- if get_option('want_btrfs_clone')
+ if get_option('want_ficlone')
has_statfs = cc.has_function(
'statfs',
prefix: '#include ',
)
- has_btrfs_header = cc.has_header('linux/btrfs.h')
- if has_statfs and has_btrfs_header
- conf.set('HAVE_LINUX_BTRFS', 1)
+ has_linux_fs_header = cc.has_header('linux/fs.h')
+ if has_statfs and has_linux_fs_header
+ conf.set('HAVE_FICLONE', 1)
else
error(
'''
- : Requirements not met for btrfs clone support.
- : If missing linux/btrfs.h, you probably need to install the linux-headers package.
- : To build without btrfs clone support and skip this check, add
- : "-Dwant_btrfs_clone=false" to the meson setup options.''',
+ : Requirements not met for reflink clone support.
+ : If missing linux/fs.h, you probably need to install the linux-headers package.
+ : To build without reflink clone support and skip this check, add
+ : "-Dwant_ficlone=false" to the meson setup options.''',
)
endif
endif
diff --git a/src/restore.c b/src/restore.c
index 5752b006..4654080c 100644
--- a/src/restore.c
+++ b/src/restore.c
@@ -30,7 +30,7 @@ along with this program. If not, see .
#include
#include "parse_cli_options.h"
-#include "btrfs.h"
+#include "ficlone.h"
#include "restore.h"
#include "utils.h"
#include "messages.h"
@@ -108,7 +108,7 @@ move_back(const char *src, const char *dest, bool want_dry_run)
else
{
/* regular file on different device -> try btrfs clone */
- int clone_res = do_btrfs_clone(src, dest, &clone_errno);
+ int clone_res = do_ficlone(src, dest, &clone_errno);
if (clone_res == 0)
{
errno = 0;
diff --git a/src/trashinfo.h b/src/trashinfo.h
index 342c8c8a..7f32c9e8 100644
--- a/src/trashinfo.h
+++ b/src/trashinfo.h
@@ -67,7 +67,7 @@ struct st_waste
*/
bool removable;
- bool is_btrfs;
+ bool is_ficlone_fs;
};
diff --git a/test/COMMON b/test/COMMON
index 7eb78c3a..dd3b9bf1 100644
--- a/test/COMMON
+++ b/test/COMMON
@@ -1,6 +1,8 @@
# shellcheck shell=sh
# included by test scripts
+SKIP=77
+
create_some_files() {
mkdir -p somefiles/topdir/dir1/dir2/dir3
diff --git a/test/conf/bcachefs_img.testrc b/test/conf/bcachefs_img.testrc
new file mode 100644
index 00000000..72b2c0b0
--- /dev/null
+++ b/test/conf/bcachefs_img.testrc
@@ -0,0 +1,4 @@
+waste = /tmp/rmw-bcachefs-loop/@two/Waste
+WASTE = $HOME/.local/share/Waste
+
+expire_age = 45
diff --git a/test/meson.build b/test/meson.build
index 95f4c7c0..a0cb7c36 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -18,8 +18,11 @@ scripts = [
'test_restore.sh',
]
-if has_statfs and has_btrfs_header
- scripts += ['test_btrfs_clone.sh']
+if host_sys == 'linux'
+ if has_statfs and has_linux_fs_header
+ scripts += ['test_btrfs_clone.sh']
+ endif
+ scripts += ['test_bcachefs.sh']
endif
RMW_FAKE_HOME = join_paths(meson.current_build_dir(), 'rmw-tests-home')
@@ -54,4 +57,3 @@ foreach s : scripts
depends: main_bin,
)
endforeach
-
diff --git a/test/rmw-bcachefs-test.img.sha256sum b/test/rmw-bcachefs-test.img.sha256sum
new file mode 100644
index 00000000..e8ad504c
--- /dev/null
+++ b/test/rmw-bcachefs-test.img.sha256sum
@@ -0,0 +1 @@
+bc7bcbd2800230995ba36394f1814e83bee2eeca579691420b7feeff653cbf7c rmw-bcachefs-test.img
diff --git a/test/test_bcachefs.sh b/test/test_bcachefs.sh
new file mode 100755
index 00000000..5ea5e964
--- /dev/null
+++ b/test/test_bcachefs.sh
@@ -0,0 +1,110 @@
+#!/bin/sh
+set -ve
+
+if [ -e COMMON ]; then
+ . ./COMMON
+else
+ . "${MESON_SOURCE_ROOT}/test/COMMON"
+fi
+
+BCACHEFS_MOUNTPOINT="/tmp/rmw-bcachefs-loop"
+BCACHEFS_IMAGE="${MESON_SOURCE_ROOT}/test/rmw-bcachefs-test.img"
+
+if ! command -v bcachefs >/dev/null 2>&1; then
+ echo "bcachefs userspace tools not found; skipping."
+ exit $SKIP
+fi
+
+if ! sudo -n true 2>/dev/null; then
+ echo "sudo not available without password; skipping bcachefs test."
+ exit $SKIP
+fi
+
+# Load the module if available, then confirm kernel support
+sudo modprobe bcachefs 2>/dev/null || true
+if ! grep -qw 'bcachefs' /proc/filesystems 2>/dev/null; then
+ echo "bcachefs not supported by kernel; skipping."
+ exit $SKIP
+fi
+
+if [ ! -f "$BCACHEFS_IMAGE" ]; then
+ echo "bcachefs test image not found at $BCACHEFS_IMAGE; skipping."
+ exit $SKIP
+fi
+
+if [ ! -d "$BCACHEFS_MOUNTPOINT" ]; then
+ sudo mkdir "$BCACHEFS_MOUNTPOINT"
+fi
+
+if mountpoint -q "$BCACHEFS_MOUNTPOINT" 2>/dev/null; then
+ echo "bcachefs mountpoint already mounted from a previous run; cleaning up."
+ sudo umount "$BCACHEFS_MOUNTPOINT"
+fi
+
+STALE_LOOP=$(sudo losetup -j "$BCACHEFS_IMAGE" -O NAME --noheadings 2>/dev/null)
+if [ -n "$STALE_LOOP" ]; then
+ echo "bcachefs image already on loop device $STALE_LOOP from a previous run; cleaning up."
+ sudo losetup -d "$STALE_LOOP"
+fi
+
+LOOP=$(sudo losetup -f --show "$BCACHEFS_IMAGE")
+sudo mount -t bcachefs "$LOOP" "$BCACHEFS_MOUNTPOINT"
+if [ ! -d "$BCACHEFS_MOUNTPOINT/@two" ]; then
+ sudo bcachefs subvolume create "$BCACHEFS_MOUNTPOINT/@two"
+fi
+sudo chown "$(id -u)" -R "$BCACHEFS_MOUNTPOINT"
+
+cd "$BCACHEFS_MOUNTPOINT"
+BCACHEFS_RMW_CMD="$BIN_DIR/rmw -c ${MESON_SOURCE_ROOT}/test/conf/bcachefs_img.testrc"
+
+BCACHEFS_SUBVOLUME="$BCACHEFS_MOUNTPOINT/@two"
+BCACHEFS_WASTE_DIR="$BCACHEFS_SUBVOLUME/Waste"
+
+rm -rf "$BCACHEFS_WASTE_DIR"
+
+# --- Test: move a file across bcachefs subvolumes (issue #526) ---
+echo "== Test: move a file across bcachefs subvolumes"
+touch foo
+$BCACHEFS_RMW_CMD foo
+test ! -f foo
+test -f "$BCACHEFS_WASTE_DIR/files/foo"
+test -f "$BCACHEFS_WASTE_DIR/info/foo.trashinfo"
+
+echo "== Test: restore the moved file"
+$BCACHEFS_RMW_CMD -u
+test -f foo
+test ! -f "$BCACHEFS_WASTE_DIR/info/foo.trashinfo"
+
+# --- Test: move a directory across bcachefs subvolumes ---
+echo "== Test: move a directory across bcachefs subvolumes"
+BCACHEFS_TEST_DIR="$BCACHEFS_MOUNTPOINT/test_dir"
+rm -rf "$BCACHEFS_TEST_DIR"
+mkdir "$BCACHEFS_TEST_DIR"
+touch "$BCACHEFS_TEST_DIR/bar"
+$BCACHEFS_RMW_CMD "$BCACHEFS_TEST_DIR"
+test ! -d "$BCACHEFS_TEST_DIR"
+test -d "$BCACHEFS_WASTE_DIR/files/test_dir"
+test -f "$BCACHEFS_WASTE_DIR/files/test_dir/bar"
+test -f "$BCACHEFS_WASTE_DIR/info/test_dir.trashinfo"
+
+echo "== Test: restore the moved directory"
+$BCACHEFS_RMW_CMD -u
+test -d "$BCACHEFS_TEST_DIR"
+test -f "$BCACHEFS_TEST_DIR/bar"
+test ! -f "$BCACHEFS_WASTE_DIR/info/test_dir.trashinfo"
+
+# --- Test: purge an expired file from bcachefs waste ---
+echo "== Test: purge an expired file from bcachefs waste"
+RMW_FAKE_YEAR=true $BCACHEFS_RMW_CMD foo
+test -f "$BCACHEFS_WASTE_DIR/files/foo"
+test -f "$BCACHEFS_WASTE_DIR/info/foo.trashinfo"
+$BCACHEFS_RMW_CMD -g
+test ! -f "$BCACHEFS_WASTE_DIR/files/foo"
+test ! -f "$BCACHEFS_WASTE_DIR/info/foo.trashinfo"
+
+cd
+
+sudo umount "$BCACHEFS_MOUNTPOINT"
+sudo losetup -d "$LOOP"
+
+exit 0