From 81ae6b1e73004f9a897bd59c95504d6b6c8b32bb Mon Sep 17 00:00:00 2001 From: wdfk-prog <1425075683@qq.com> Date: Tue, 5 May 2026 20:28:10 +0800 Subject: [PATCH 1/4] feat: add DFS v2 compatibility support add DFS v2 mount, dentry, vnode and filesystem type registration paths for littlefs centralize DFS ABI differences across DFS v2, RT-Thread 5.x DFS v1 and legacy DFS layouts adapt read/write/lseek signatures and explicit offset handling for DFS v2 keep legacy DFS v1 behavior through compatibility macros verify basic build and QEMU littlefs DFS v2 smoke flow on bsp/qemu-vexpress-a9 --- dfs_lfs.c | 571 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 476 insertions(+), 95 deletions(-) diff --git a/dfs_lfs.c b/dfs_lfs.c index 25819a96..75811396 100644 --- a/dfs_lfs.c +++ b/dfs_lfs.c @@ -3,6 +3,12 @@ #include #include +#if defined(RT_USING_DFS_V2) +#include +#include +#else +#include +#endif /* defined(RT_USING_DFS_V2) */ #include "lfs.h" @@ -12,12 +18,165 @@ #if defined(RT_VERSION_CHECK) && (RTTHREAD_VERSION >= RT_VERSION_CHECK(5, 0, 2)) #define DFS_LFS_RW_RETURN_TYPE ssize_t #define DFS_LFS_LSK_RETURN_TYPE off_t -#define DFS_LFS_MKFS(dev_id, fs_name) _dfs_lfs_mkfs(dev_id, fs_name) #else #define DFS_LFS_RW_RETURN_TYPE int #define DFS_LFS_LSK_RETURN_TYPE int -#define DFS_LFS_MKFS(dev_id, fs_name) _dfs_lfs_mkfs(dev_id) -#endif +#endif /* defined(RT_VERSION_CHECK) && (RTTHREAD_VERSION >= RT_VERSION_CHECK(5, 0, 2)) */ + +#if defined(RT_USING_DFS_V2) || (defined(RT_VERSION_CHECK) && (RTTHREAD_VERSION >= RT_VERSION_CHECK(5, 0, 0))) +#define DFS_LFS_MKFS_PARAMS rt_device_t dev_id, const char *fs_name +#define DFS_LFS_MKFS_HAS_FS_NAME 1 +#else +#define DFS_LFS_MKFS_PARAMS rt_device_t dev_id +#define DFS_LFS_MKFS_HAS_FS_NAME 0 +#endif /* defined(RT_USING_DFS_V2) || (defined(RT_VERSION_CHECK) && (RTTHREAD_VERSION >= RT_VERSION_CHECK(5, 0, 0))) */ + +/** + * @brief Keep DFS v1/v2 ABI differences in one place. + * + * RT-Thread has several incompatible DFS file-operation ABIs: + * + * - DFS v2 uses struct dfs_mnt and dentry based paths. + * - RT-Thread 5.0.2 and later use struct dfs_file for non-DFS-v2 builds. + * - RT-Thread 5.x builds that define RT_USING_DFS_V1 also use struct dfs_file, + * even though the selected filesystem profile is DFS v1. + * - RT-Thread 5.0.0 without RT_USING_DFS_V1 is a transition ABI: file + * callbacks still use struct dfs_fd, but path/type/size/fs live in vnode. + * - RT-Thread 4.x and older legacy DFS builds use struct dfs_fd with direct + * fd fields such as path, type, size and fs. + */ +#if defined(RT_USING_DFS_V2) +#define DFS_LFS_FS_FLAGS FS_NEED_DEVICE +#define DFS_LFS_MNT_TYPE struct dfs_mnt +#define DFS_LFS_MNT_DEV(mnt) ((mnt)->dev_id) +#define DFS_LFS_MNT_DATA(mnt) ((mnt)->data) +#define DFS_LFS_SET_MNT_DATA(mnt, value) ((mnt)->data = (value)) +#define DFS_LFS_FILE_STRUCT struct dfs_file +#define DFS_LFS_FILE_PATH(file) ((file)->dentry->pathname) +#define DFS_LFS_FILE_POS(file) ((file)->fpos) +#define DFS_LFS_FILE_SIZE(file) ((file)->vnode->size) +#define DFS_LFS_FILE_TYPE(file) ((file)->vnode->type) +#define DFS_LFS_FILE_REF_COUNT(file) ((file)->vnode->ref_count) +#define DFS_LFS_FILE_IS_VALID(file) ((file)->vnode != RT_NULL) +#define DFS_LFS_FILE_FS(file) ((file)->dentry->mnt) +#define DFS_LFS_READ_PARAMS DFS_LFS_FILE_STRUCT* file, void* buf, size_t len, off_t* pos +#define DFS_LFS_WRITE_PARAMS DFS_LFS_FILE_STRUCT* file, const void* buf, size_t len, off_t* pos +#define DFS_LFS_LSEEK_PARAMS DFS_LFS_FILE_STRUCT* file, off_t offset, int whence +#define DFS_LFS_IO_POS_PARAM pos +#define DFS_LFS_LSEEK_WHENCE whence +#define DFS_LFS_STORE_FILE_POS(file, pos, lfs_pos) (*(pos) = (off_t)(lfs_pos)) +#define DFS_LFS_REGISTER() dfs_register(&_dfs_lfs_type) +#elif defined(RT_VERSION_CHECK) && (RTTHREAD_VERSION >= RT_VERSION_CHECK(5, 0, 2)) +/** + * @brief RT-Thread 5.0.2 and later non-DFS-v2 builds use struct dfs_file + * callbacks with vnode-backed file metadata. + */ +#define DFS_LFS_MNT_TYPE struct dfs_filesystem +#define DFS_LFS_MNT_DEV(mnt) ((mnt)->dev_id) +#define DFS_LFS_MNT_DATA(mnt) ((mnt)->data) +#define DFS_LFS_SET_MNT_DATA(mnt, value) ((mnt)->data = (value)) +#define DFS_LFS_FILE_STRUCT struct dfs_file +#define DFS_LFS_FILE_PATH(file) ((file)->vnode->path) +#define DFS_LFS_FILE_POS(file) ((file)->pos) +#define DFS_LFS_FILE_SIZE(file) ((file)->vnode->size) +#define DFS_LFS_FILE_TYPE(file) ((file)->vnode->type) +#define DFS_LFS_FILE_REF_COUNT(file) ((file)->vnode->ref_count) +#define DFS_LFS_FILE_IS_VALID(file) ((file)->vnode != RT_NULL) +#define DFS_LFS_FILE_FS(file) ((file)->vnode->fs) +#define DFS_LFS_READ_PARAMS DFS_LFS_FILE_STRUCT* file, void* buf, size_t len +#define DFS_LFS_WRITE_PARAMS DFS_LFS_FILE_STRUCT* file, const void* buf, size_t len +#define DFS_LFS_LSEEK_PARAMS DFS_LFS_FILE_STRUCT* file, rt_off_t offset +#define DFS_LFS_IO_POS_PARAM RT_NULL +#define DFS_LFS_LSEEK_WHENCE SEEK_SET +#define DFS_LFS_STORE_FILE_POS(file, pos, lfs_pos) (DFS_LFS_FILE_POS(file) = (rt_off_t)(lfs_pos)) +#define DFS_LFS_FS_FLAGS DFS_FS_FLAG_DEFAULT +#define DFS_LFS_REGISTER() dfs_register(&_dfs_lfs_ops) +#elif defined(RT_USING_DFS_V1) +/** + * @brief RT-Thread 5.x DFS v1 split builds use struct dfs_file callbacks. + * + * This branch is for versions where DFS v1/v2 are explicit Kconfig profiles. + * For example, RT-Thread v5.0.1 with RT_USING_DFS_V1 enabled still uses the + * DFS v1 profile, but its file operation callbacks take struct dfs_file and + * file metadata is stored under vnode. + */ +#define DFS_LFS_MNT_TYPE struct dfs_filesystem +#define DFS_LFS_MNT_DEV(mnt) ((mnt)->dev_id) +#define DFS_LFS_MNT_DATA(mnt) ((mnt)->data) +#define DFS_LFS_SET_MNT_DATA(mnt, value) ((mnt)->data = (value)) +#define DFS_LFS_FILE_STRUCT struct dfs_file +#define DFS_LFS_FILE_PATH(file) ((file)->vnode->path) +#define DFS_LFS_FILE_POS(file) ((file)->pos) +#define DFS_LFS_FILE_SIZE(file) ((file)->vnode->size) +#define DFS_LFS_FILE_TYPE(file) ((file)->vnode->type) +#define DFS_LFS_FILE_REF_COUNT(file) ((file)->vnode->ref_count) +#define DFS_LFS_FILE_IS_VALID(file) ((file)->vnode != RT_NULL) +#define DFS_LFS_FILE_FS(file) ((file)->vnode->fs) +#define DFS_LFS_READ_PARAMS DFS_LFS_FILE_STRUCT* file, void* buf, size_t len +#define DFS_LFS_WRITE_PARAMS DFS_LFS_FILE_STRUCT* file, const void* buf, size_t len +#define DFS_LFS_LSEEK_PARAMS DFS_LFS_FILE_STRUCT* file, rt_off_t offset +#define DFS_LFS_IO_POS_PARAM RT_NULL +#define DFS_LFS_LSEEK_WHENCE SEEK_SET +#define DFS_LFS_STORE_FILE_POS(file, pos, lfs_pos) (DFS_LFS_FILE_POS(file) = (rt_off_t)(lfs_pos)) +#define DFS_LFS_FS_FLAGS DFS_FS_FLAG_DEFAULT +#define DFS_LFS_REGISTER() dfs_register(&_dfs_lfs_ops) +#elif defined(RT_VERSION_CHECK) && (RTTHREAD_VERSION >= RT_VERSION_CHECK(5, 0, 0)) +/** + * @brief RT-Thread 5.0.0 transition builds use struct dfs_fd callbacks. + * + * This branch is for 5.x transition builds that do not expose RT_USING_DFS_V1. + * The callback parameter is still struct dfs_fd, but file metadata has already + * moved to vnode. This differs from older RT-Thread 4.x legacy DFS layouts. + */ +#define DFS_LFS_MNT_TYPE struct dfs_filesystem +#define DFS_LFS_MNT_DEV(mnt) ((mnt)->dev_id) +#define DFS_LFS_MNT_DATA(mnt) ((mnt)->data) +#define DFS_LFS_SET_MNT_DATA(mnt, value) ((mnt)->data = (value)) +#define DFS_LFS_FILE_STRUCT struct dfs_fd +#define DFS_LFS_FILE_PATH(file) ((file)->vnode->path) +#define DFS_LFS_FILE_POS(file) ((file)->pos) +#define DFS_LFS_FILE_SIZE(file) ((file)->vnode->size) +#define DFS_LFS_FILE_TYPE(file) ((file)->vnode->type) +#define DFS_LFS_FILE_REF_COUNT(file) ((file)->ref_count) +#define DFS_LFS_FILE_IS_VALID(file) ((file)->vnode != RT_NULL) +#define DFS_LFS_FILE_FS(file) ((file)->vnode->fs) +#define DFS_LFS_READ_PARAMS DFS_LFS_FILE_STRUCT* file, void* buf, size_t len +#define DFS_LFS_WRITE_PARAMS DFS_LFS_FILE_STRUCT* file, const void* buf, size_t len +#define DFS_LFS_LSEEK_PARAMS DFS_LFS_FILE_STRUCT* file, rt_off_t offset +#define DFS_LFS_IO_POS_PARAM RT_NULL +#define DFS_LFS_LSEEK_WHENCE SEEK_SET +#define DFS_LFS_STORE_FILE_POS(file, pos, lfs_pos) (DFS_LFS_FILE_POS(file) = (rt_off_t)(lfs_pos)) +#define DFS_LFS_FS_FLAGS DFS_FS_FLAG_DEFAULT +#define DFS_LFS_REGISTER() dfs_register(&_dfs_lfs_ops) +#else +/** + * @brief RT-Thread 4.x and older legacy DFS builds. + * + * These versions do not provide the DFS v1/v2 split macros. File operation + * callbacks use struct dfs_fd, and path/type/size/fs are direct struct dfs_fd + * fields instead of vnode-backed fields. + */ +#define DFS_LFS_MNT_TYPE struct dfs_filesystem +#define DFS_LFS_MNT_DEV(mnt) ((mnt)->dev_id) +#define DFS_LFS_MNT_DATA(mnt) ((mnt)->data) +#define DFS_LFS_SET_MNT_DATA(mnt, value) ((mnt)->data = (value)) +#define DFS_LFS_FILE_STRUCT struct dfs_fd +#define DFS_LFS_FILE_PATH(file) ((file)->path) +#define DFS_LFS_FILE_POS(file) ((file)->pos) +#define DFS_LFS_FILE_SIZE(file) ((file)->size) +#define DFS_LFS_FILE_TYPE(file) ((file)->type) +#define DFS_LFS_FILE_REF_COUNT(file) ((file)->ref_count) +#define DFS_LFS_FILE_IS_VALID(file) RT_TRUE +#define DFS_LFS_FILE_FS(file) ((file)->fs) +#define DFS_LFS_READ_PARAMS DFS_LFS_FILE_STRUCT* file, void* buf, size_t len +#define DFS_LFS_WRITE_PARAMS DFS_LFS_FILE_STRUCT* file, const void* buf, size_t len +#define DFS_LFS_LSEEK_PARAMS DFS_LFS_FILE_STRUCT* file, rt_off_t offset +#define DFS_LFS_IO_POS_PARAM RT_NULL +#define DFS_LFS_LSEEK_WHENCE SEEK_SET +#define DFS_LFS_STORE_FILE_POS(file, pos, lfs_pos) (DFS_LFS_FILE_POS(file) = (rt_off_t)(lfs_pos)) +#define DFS_LFS_FS_FLAGS DFS_FS_FLAG_DEFAULT +#define DFS_LFS_REGISTER() dfs_register(&_dfs_lfs_ops) +#endif /* defined(RT_USING_DFS_V2) */ #ifndef RT_DEF_LFS_DRIVERS #define RT_DEF_LFS_DRIVERS 1 @@ -259,6 +418,49 @@ static int _lfs_result_to_dfs(int result) return status; } +/* DFS v2 passes read/write offsets explicitly. Keep DFS v1 behavior unchanged. */ +static int _dfs_lfs_sync_file_pos(DFS_LFS_FILE_STRUCT* file, + dfs_lfs_fd_t* dfs_lfs_fd, + off_t* pos) +{ +#if defined(RT_USING_DFS_V2) + lfs_soff_t current; + lfs_soff_t target; + lfs_soff_t result; + + RT_ASSERT(file != RT_NULL); + RT_ASSERT(dfs_lfs_fd != RT_NULL); + RT_ASSERT(pos != RT_NULL); + + target = (lfs_soff_t)*pos; + current = lfs_file_tell(dfs_lfs_fd->lfs, &dfs_lfs_fd->u.file); + if (current < 0) + { + return _lfs_result_to_dfs((int)current); + } + + if (current == target) + { + return RT_EOK; + } + + result = lfs_file_seek(dfs_lfs_fd->lfs, + &dfs_lfs_fd->u.file, + target, + LFS_SEEK_SET); + if (result < 0) + { + return _lfs_result_to_dfs((int)result); + } +#else + (void)file; + (void)dfs_lfs_fd; + (void)pos; +#endif /* defined(RT_USING_DFS_V2) */ + + return RT_EOK; +} + static void _lfs_load_config(struct lfs_config* lfs_cfg, struct rt_mtd_nor_device* mtd_nor) { uint64_t mtd_size; @@ -296,14 +498,14 @@ static void _lfs_load_config(struct lfs_config* lfs_cfg, struct rt_mtd_nor_devic lfs_cfg->sync = _lfs_flash_sync; } -static int _dfs_lfs_mount(struct dfs_filesystem* dfs, unsigned long rwflag, const void* data) +static int _dfs_lfs_mount(DFS_LFS_MNT_TYPE* dfs, unsigned long rwflag, const void* data) { int result; int index; dfs_lfs_t* dfs_lfs; /* Check Device Type */ - if (dfs->dev_id->type != RT_Device_Class_MTD) + if (DFS_LFS_MNT_DEV(dfs)->type != RT_Device_Class_MTD) { rt_kprintf("The flash device type must be MTD!\n"); return -EINVAL; @@ -325,7 +527,7 @@ static int _dfs_lfs_mount(struct dfs_filesystem* dfs, unsigned long rwflag, cons } rt_memset(dfs_lfs, 0, sizeof(dfs_lfs_t)); rt_mutex_init(&dfs_lfs->lock, "lfslock", RT_IPC_FLAG_PRIO); - _lfs_load_config(&dfs_lfs->cfg, (struct rt_mtd_nor_device*)dfs->dev_id); + _lfs_load_config(&dfs_lfs->cfg, (struct rt_mtd_nor_device*)DFS_LFS_MNT_DEV(dfs)); /* mount lfs*/ result = lfs_mount(&dfs_lfs->lfs, &dfs_lfs->cfg); @@ -339,30 +541,30 @@ static int _dfs_lfs_mount(struct dfs_filesystem* dfs, unsigned long rwflag, cons } /* mount succeed! */ - dfs->data = (void*)dfs_lfs; + DFS_LFS_SET_MNT_DATA(dfs, (void*)dfs_lfs); _lfs_mount_tbl[index] = dfs_lfs; return RT_EOK; } -static int _dfs_lfs_unmount(struct dfs_filesystem* dfs) +static int _dfs_lfs_unmount(DFS_LFS_MNT_TYPE* dfs) { int result; int index; dfs_lfs_t* dfs_lfs; RT_ASSERT(dfs != RT_NULL); - RT_ASSERT(dfs->data != RT_NULL); + RT_ASSERT(DFS_LFS_MNT_DATA(dfs) != RT_NULL); /* find the device index and then umount it */ - index = _get_disk(dfs->dev_id); + index = _get_disk(DFS_LFS_MNT_DEV(dfs)); if (index == -1) { return -ENOENT; } _lfs_mount_tbl[index] = RT_NULL; - dfs_lfs = (dfs_lfs_t*)dfs->data; - dfs->data = RT_NULL; + dfs_lfs = (dfs_lfs_t*)DFS_LFS_MNT_DATA(dfs); + DFS_LFS_SET_MNT_DATA(dfs, RT_NULL); result = lfs_unmount(&dfs_lfs->lfs); rt_mutex_detach(&dfs_lfs->lock); @@ -372,12 +574,16 @@ static int _dfs_lfs_unmount(struct dfs_filesystem* dfs) } #ifndef LFS_READONLY -static int DFS_LFS_MKFS(rt_device_t dev_id, const char *fs_name) +static int _dfs_lfs_mkfs(DFS_LFS_MKFS_PARAMS) { int result; int index; dfs_lfs_t* dfs_lfs; +#if DFS_LFS_MKFS_HAS_FS_NAME + (void)fs_name; +#endif /* DFS_LFS_MKFS_HAS_FS_NAME */ + if (dev_id == RT_NULL) { return -EINVAL; @@ -448,7 +654,7 @@ static int _dfs_lfs_statfs_count(void* p, lfs_block_t b) return 0; } -static int _dfs_lfs_statfs(struct dfs_filesystem* dfs, struct statfs* buf) +static int _dfs_lfs_statfs(DFS_LFS_MNT_TYPE* dfs, struct statfs* buf) { dfs_lfs_t* dfs_lfs; int result; @@ -456,9 +662,9 @@ static int _dfs_lfs_statfs(struct dfs_filesystem* dfs, struct statfs* buf) RT_ASSERT(buf != RT_NULL); RT_ASSERT(dfs != RT_NULL); - RT_ASSERT(dfs->data != RT_NULL); + RT_ASSERT(DFS_LFS_MNT_DATA(dfs) != RT_NULL); - dfs_lfs = (dfs_lfs_t*)dfs->data; + dfs_lfs = (dfs_lfs_t*)DFS_LFS_MNT_DATA(dfs); /* Get total sectors and free sectors */ result = lfs_fs_traverse(&dfs_lfs->lfs, _dfs_lfs_statfs_count, &in_use); @@ -475,15 +681,15 @@ static int _dfs_lfs_statfs(struct dfs_filesystem* dfs, struct statfs* buf) } #ifndef LFS_READONLY -static int _dfs_lfs_unlink(struct dfs_filesystem* dfs, const char* path) +static int _dfs_lfs_unlink(DFS_LFS_MNT_TYPE* dfs, const char* path) { dfs_lfs_t* dfs_lfs; int result; RT_ASSERT(dfs != RT_NULL); - RT_ASSERT(dfs->data != RT_NULL); + RT_ASSERT(DFS_LFS_MNT_DATA(dfs) != RT_NULL); - dfs_lfs = (dfs_lfs_t*)dfs->data; + dfs_lfs = (dfs_lfs_t*)DFS_LFS_MNT_DATA(dfs); result = lfs_remove(&dfs_lfs->lfs, path); return _lfs_result_to_dfs(result); @@ -513,16 +719,16 @@ static void _dfs_lfs_tostat(struct stat* st, struct lfs_info* info, time_t mtime st->st_mtime = mtime; } -static int _dfs_lfs_stat(struct dfs_filesystem* dfs, const char* path, struct stat* st) +static int _dfs_lfs_stat(DFS_LFS_MNT_TYPE* dfs, const char* path, struct stat* st) { dfs_lfs_t* dfs_lfs; int result; struct lfs_info info; RT_ASSERT(dfs != RT_NULL); - RT_ASSERT(dfs->data != RT_NULL); + RT_ASSERT(DFS_LFS_MNT_DATA(dfs) != RT_NULL); - dfs_lfs = (dfs_lfs_t*)dfs->data; + dfs_lfs = (dfs_lfs_t*)DFS_LFS_MNT_DATA(dfs); result = lfs_stat(&dfs_lfs->lfs, path, &info); if (result != LFS_ERR_OK) @@ -538,15 +744,15 @@ static int _dfs_lfs_stat(struct dfs_filesystem* dfs, const char* path, struct st } #ifndef LFS_READONLY -static int _dfs_lfs_rename(struct dfs_filesystem* dfs, const char* from, const char* to) +static int _dfs_lfs_rename(DFS_LFS_MNT_TYPE* dfs, const char* from, const char* to) { dfs_lfs_t* dfs_lfs; int result; RT_ASSERT(dfs != RT_NULL); - RT_ASSERT(dfs->data != RT_NULL); + RT_ASSERT(DFS_LFS_MNT_DATA(dfs) != RT_NULL); - dfs_lfs = (dfs_lfs_t*)dfs->data; + dfs_lfs = (dfs_lfs_t*)DFS_LFS_MNT_DATA(dfs); result = lfs_rename(&dfs_lfs->lfs, from, to); return _lfs_result_to_dfs(result); @@ -556,29 +762,37 @@ static int _dfs_lfs_rename(struct dfs_filesystem* dfs, const char* from, const c /****************************************************************************** * file operations ******************************************************************************/ -static int _dfs_lfs_open(struct dfs_file* file) +static int _dfs_lfs_open(DFS_LFS_FILE_STRUCT* file) { - struct dfs_filesystem* dfs; dfs_lfs_t* dfs_lfs; + const char* path; int result; int flags = 0; RT_ASSERT(file != RT_NULL); - - dfs = (struct dfs_filesystem*)file->vnode->fs; - - RT_ASSERT(file->vnode->ref_count > 0); - if (file->vnode->ref_count > 1) + RT_ASSERT(DFS_LFS_FILE_IS_VALID(file)); + RT_ASSERT(DFS_LFS_FILE_REF_COUNT(file) > 0); + if (DFS_LFS_FILE_REF_COUNT(file) > 1) { - if (file->vnode->type == FT_DIRECTORY + if (DFS_LFS_FILE_TYPE(file) == FT_DIRECTORY && !(file->flags & O_DIRECTORY)) { return -ENOENT; } - file->pos = 0; + DFS_LFS_FILE_POS(file) = 0; } - dfs_lfs = (dfs_lfs_t*)dfs->data; +#if defined(RT_USING_DFS_V2) + RT_ASSERT(file->dentry != RT_NULL); + RT_ASSERT(file->dentry->mnt != RT_NULL); + RT_ASSERT(DFS_LFS_MNT_DATA(file->dentry->mnt) != RT_NULL); + dfs_lfs = (dfs_lfs_t*)DFS_LFS_MNT_DATA(file->dentry->mnt); +#else + RT_ASSERT(DFS_LFS_FILE_FS(file) != RT_NULL); + RT_ASSERT(DFS_LFS_MNT_DATA(DFS_LFS_FILE_FS(file)) != RT_NULL); + dfs_lfs = (dfs_lfs_t*)DFS_LFS_MNT_DATA(DFS_LFS_FILE_FS(file)); +#endif /* defined(RT_USING_DFS_V2) */ + path = DFS_LFS_FILE_PATH(file); if (file->flags & O_DIRECTORY) { @@ -596,7 +810,7 @@ static int _dfs_lfs_open(struct dfs_file* file) if (file->flags & O_CREAT) { #ifndef LFS_READONLY - result = lfs_mkdir(dfs_lfs_fd->lfs, file->vnode->path); + result = lfs_mkdir(dfs_lfs_fd->lfs, path); #else result = -EINVAL; #endif @@ -607,11 +821,11 @@ static int _dfs_lfs_open(struct dfs_file* file) else { time_t now = time(RT_NULL); - lfs_setattr(dfs_lfs_fd->lfs, file->vnode->path, ATTR_TIMESTAMP, &now, sizeof(time_t)); + lfs_setattr(dfs_lfs_fd->lfs, DFS_LFS_FILE_PATH(file), ATTR_TIMESTAMP, &now, sizeof(time_t)); } } - result = lfs_dir_open(dfs_lfs_fd->lfs, &dfs_lfs_fd->u.dir, file->vnode->path); + result = lfs_dir_open(dfs_lfs_fd->lfs, &dfs_lfs_fd->u.dir, path); if (result != LFS_ERR_OK) { goto _error_dir; @@ -658,7 +872,7 @@ static int _dfs_lfs_open(struct dfs_file* file) if (file->flags & O_APPEND) flags |= LFS_O_APPEND; - result = lfs_file_open(dfs_lfs_fd->lfs, &dfs_lfs_fd->u.file, file->vnode->path, flags); + result = lfs_file_open(dfs_lfs_fd->lfs, &dfs_lfs_fd->u.file, path, flags); if (result != LFS_ERR_OK) { goto _error_file; @@ -666,8 +880,8 @@ static int _dfs_lfs_open(struct dfs_file* file) else { file->data = (void*)dfs_lfs_fd; - file->pos = dfs_lfs_fd->u.file.pos; - file->vnode->size = dfs_lfs_fd->u.file.ctz.size; + DFS_LFS_FILE_POS(file) = dfs_lfs_fd->u.file.pos; + DFS_LFS_FILE_SIZE(file) = dfs_lfs_fd->u.file.ctz.size; return RT_EOK; } @@ -681,7 +895,7 @@ static int _dfs_lfs_open(struct dfs_file* file) } } -static int _dfs_lfs_close(struct dfs_file* file) +static int _dfs_lfs_close(DFS_LFS_FILE_STRUCT* file) { int result; dfs_lfs_fd_t* dfs_lfs_fd; @@ -689,11 +903,11 @@ static int _dfs_lfs_close(struct dfs_file* file) RT_ASSERT(file != RT_NULL); RT_ASSERT(file->data != RT_NULL); - RT_ASSERT(file->vnode->ref_count > 0); + RT_ASSERT(DFS_LFS_FILE_REF_COUNT(file) > 0); dfs_lfs_fd = (dfs_lfs_fd_t*)file->data; - if (file->vnode->type == FT_DIRECTORY) + if (DFS_LFS_FILE_TYPE(file) == FT_DIRECTORY) { result = lfs_dir_close(dfs_lfs_fd->lfs, &dfs_lfs_fd->u.dir); } @@ -704,7 +918,7 @@ static int _dfs_lfs_close(struct dfs_file* file) if (result == LFS_ERR_OK && need_time_update) { time_t now = time(RT_NULL); - lfs_setattr(dfs_lfs_fd->lfs, file->vnode->path, ATTR_TIMESTAMP, &now, sizeof(time_t)); + lfs_setattr(dfs_lfs_fd->lfs, DFS_LFS_FILE_PATH(file), ATTR_TIMESTAMP, &now, sizeof(time_t)); } } @@ -714,7 +928,7 @@ static int _dfs_lfs_close(struct dfs_file* file) return _lfs_result_to_dfs(result); } -static int _dfs_lfs_ioctl(struct dfs_file* file, int cmd, void* args) +static int _dfs_lfs_ioctl(DFS_LFS_FILE_STRUCT* file, int cmd, void* args) { switch (cmd) { @@ -731,7 +945,7 @@ static int _dfs_lfs_ioctl(struct dfs_file* file, int cmd, void* args) RT_ASSERT(file != RT_NULL); RT_ASSERT(file->data != RT_NULL); - if (file->vnode->type == FT_DIRECTORY) + if (DFS_LFS_FILE_TYPE(file) == FT_DIRECTORY) { return -EISDIR; } @@ -765,14 +979,15 @@ static int _dfs_lfs_ioctl(struct dfs_file* file, int cmd, void* args) { return _lfs_result_to_dfs((int)pos); } - file->pos = (rt_off_t)pos; + DFS_LFS_FILE_POS(file) = (rt_off_t)pos; pos = lfs_file_size(dfs_lfs_fd->lfs, &dfs_lfs_fd->u.file); if (pos < 0) { return _lfs_result_to_dfs((int)pos); } - file->vnode->size = (rt_off_t)pos; + DFS_LFS_FILE_SIZE(file) = (rt_off_t)pos; + return RT_EOK; #endif } @@ -784,85 +999,84 @@ static int _dfs_lfs_ioctl(struct dfs_file* file, int cmd, void* args) return -ENOSYS; } -static DFS_LFS_RW_RETURN_TYPE _dfs_lfs_read(struct dfs_file* file, void* buf, size_t len) +#if defined(RT_USING_DFS_V2) && !defined(LFS_READONLY) +static int _dfs_lfs_truncate(DFS_LFS_FILE_STRUCT* file, off_t length) +{ + return _dfs_lfs_ioctl(file, RT_FIOFTRUNCATE, &length); +} +#endif /* defined(RT_USING_DFS_V2) && !defined(LFS_READONLY) */ + +static DFS_LFS_RW_RETURN_TYPE _dfs_lfs_read(DFS_LFS_READ_PARAMS) { + int result; lfs_ssize_t ssize; dfs_lfs_fd_t* dfs_lfs_fd; RT_ASSERT(file != RT_NULL); RT_ASSERT(file->data != RT_NULL); - if (file->vnode->type == FT_DIRECTORY) + if (DFS_LFS_FILE_TYPE(file) == FT_DIRECTORY) { return -EISDIR; } dfs_lfs_fd = (dfs_lfs_fd_t*)file->data; -#if 0 - if (lfs_file_tell(dfs_lfs_fd->lfs, &dfs_lfs_fd->u.file) != file->pos) + result = _dfs_lfs_sync_file_pos(file, dfs_lfs_fd, DFS_LFS_IO_POS_PARAM); + if (result < 0) { - lfs_soff_t soff = lfs_file_seek(dfs_lfs_fd->lfs, &dfs_lfs_fd->u.file, file->pos, LFS_SEEK_SET); - if (soff < 0) - { - return _lfs_result_to_dfs(soff); - } + return result; } -#endif ssize = lfs_file_read(dfs_lfs_fd->lfs, &dfs_lfs_fd->u.file, buf, len); if (ssize < 0) { - return _lfs_result_to_dfs(ssize); + return _lfs_result_to_dfs((int)ssize); } /* update position */ - file->pos = dfs_lfs_fd->u.file.pos; + DFS_LFS_STORE_FILE_POS(file, DFS_LFS_IO_POS_PARAM, dfs_lfs_fd->u.file.pos); return ssize; } #ifndef LFS_READONLY -static DFS_LFS_RW_RETURN_TYPE _dfs_lfs_write(struct dfs_file* file, const void* buf, size_t len) +static DFS_LFS_RW_RETURN_TYPE _dfs_lfs_write(DFS_LFS_WRITE_PARAMS) { + int result; lfs_ssize_t ssize; dfs_lfs_fd_t* dfs_lfs_fd; RT_ASSERT(file != RT_NULL); RT_ASSERT(file->data != RT_NULL); - if (file->vnode->type == FT_DIRECTORY) + if (DFS_LFS_FILE_TYPE(file) == FT_DIRECTORY) { return -EISDIR; } dfs_lfs_fd = (dfs_lfs_fd_t*)file->data; -#if 0 - if (lfs_file_tell(dfs_lfs_fd->lfs, &dfs_lfs_fd->u.file) != file->pos) + result = _dfs_lfs_sync_file_pos(file, dfs_lfs_fd, DFS_LFS_IO_POS_PARAM); + if (result < 0) { - lfs_soff_t soff = lfs_file_seek(dfs_lfs_fd->lfs, &dfs_lfs_fd->u.file, file->pos, LFS_SEEK_SET); - if (soff < 0) - { - return _lfs_result_to_dfs(soff); - } + return result; } -#endif ssize = lfs_file_write(dfs_lfs_fd->lfs, &dfs_lfs_fd->u.file, buf, len); if (ssize < 0) { - return _lfs_result_to_dfs(ssize); + return _lfs_result_to_dfs((int)ssize); } /* update position and file size */ - file->pos = dfs_lfs_fd->u.file.pos; - file->vnode->size = dfs_lfs_fd->u.file.ctz.size; + DFS_LFS_STORE_FILE_POS(file, DFS_LFS_IO_POS_PARAM, dfs_lfs_fd->u.file.pos); + DFS_LFS_FILE_SIZE(file) = dfs_lfs_fd->u.file.ctz.size; return ssize; } #endif -static int _dfs_lfs_flush(struct dfs_file* file) +static int _dfs_lfs_flush(DFS_LFS_FILE_STRUCT* file) { int result; dfs_lfs_fd_t* dfs_lfs_fd; @@ -877,13 +1091,13 @@ static int _dfs_lfs_flush(struct dfs_file* file) if (result == LFS_ERR_OK && need_time_update) { time_t now = time(RT_NULL); - lfs_setattr(dfs_lfs_fd->lfs, file->vnode->path, ATTR_TIMESTAMP, &now, sizeof(time_t)); + lfs_setattr(dfs_lfs_fd->lfs, DFS_LFS_FILE_PATH(file), ATTR_TIMESTAMP, &now, sizeof(time_t)); } return _lfs_result_to_dfs(result); } -static DFS_LFS_LSK_RETURN_TYPE _dfs_lfs_lseek(struct dfs_file* file, rt_off_t offset) +static DFS_LFS_LSK_RETURN_TYPE _dfs_lfs_lseek(DFS_LFS_LSEEK_PARAMS) { dfs_lfs_fd_t* dfs_lfs_fd; @@ -892,34 +1106,50 @@ static DFS_LFS_LSK_RETURN_TYPE _dfs_lfs_lseek(struct dfs_file* file, rt_off_t of dfs_lfs_fd = (dfs_lfs_fd_t*)file->data; - if (file->vnode->type == FT_REGULAR) +#if defined(RT_USING_DFS_V2) + switch (DFS_LFS_LSEEK_WHENCE) + { + case SEEK_SET: + break; + case SEEK_CUR: + offset += DFS_LFS_FILE_POS(file); + break; + case SEEK_END: + offset += DFS_LFS_FILE_SIZE(file); + break; + default: + return -EINVAL; + } +#endif /* defined(RT_USING_DFS_V2) */ + + if (DFS_LFS_FILE_TYPE(file) == FT_REGULAR) { lfs_soff_t soff = lfs_file_seek(dfs_lfs_fd->lfs, &dfs_lfs_fd->u.file, offset, LFS_SEEK_SET); if (soff < 0) { - return _lfs_result_to_dfs(soff); + return _lfs_result_to_dfs((int)soff); } - file->pos = dfs_lfs_fd->u.file.pos; + DFS_LFS_FILE_POS(file) = dfs_lfs_fd->u.file.pos; } - else if (file->vnode->type == FT_DIRECTORY) + else if (DFS_LFS_FILE_TYPE(file) == FT_DIRECTORY) { /* skip . and .. */ lfs_off_t off = offset / sizeof(struct dirent) + 2; lfs_soff_t soff = lfs_dir_seek(dfs_lfs_fd->lfs, &dfs_lfs_fd->u.dir, off); if (soff < 0) { - return _lfs_result_to_dfs(soff); + return _lfs_result_to_dfs((int)soff); } - /* file->pos must stay in user-space: remove the two dot entries accounted for in dir.pos */ - file->pos = (dfs_lfs_fd->u.dir.pos > 2 ? dfs_lfs_fd->u.dir.pos - 2 : 0) * sizeof(struct dirent); + /* DFS_LFS_FILE_POS(file) must stay in user-space: remove the two dot entries accounted for in dir.pos */ + DFS_LFS_FILE_POS(file) = (dfs_lfs_fd->u.dir.pos > 2 ? dfs_lfs_fd->u.dir.pos - 2 : 0) * sizeof(struct dirent); } - return (file->pos); + return (DFS_LFS_FILE_POS(file)); } -static int _dfs_lfs_getdents(struct dfs_file* file, struct dirent* dirp, uint32_t count) +static int _dfs_lfs_getdents(DFS_LFS_FILE_STRUCT* file, struct dirent* dirp, uint32_t count) { dfs_lfs_fd_t* dfs_lfs_fd; int result; @@ -987,11 +1217,161 @@ static int _dfs_lfs_getdents(struct dfs_file* file, struct dirent* dirp, uint32_ return _lfs_result_to_dfs(result); } - file->pos += index * sizeof(struct dirent); + DFS_LFS_FILE_POS(file) += index * sizeof(struct dirent); return index * sizeof(struct dirent); } +static const struct dfs_file_ops _dfs_lfs_fops; + +#if defined(RT_USING_DFS_V2) +#ifndef LFS_READONLY +static int _dfs_lfs_dentry_unlink(struct dfs_dentry* dentry) +{ + RT_ASSERT(dentry != RT_NULL); + RT_ASSERT(dentry->mnt != RT_NULL); + + return _dfs_lfs_unlink(dentry->mnt, dentry->pathname); +} + +static int _dfs_lfs_dentry_rename(struct dfs_dentry* old_dentry, struct dfs_dentry* new_dentry) +{ + RT_ASSERT(old_dentry != RT_NULL); + RT_ASSERT(old_dentry->mnt != RT_NULL); + RT_ASSERT(new_dentry != RT_NULL); + + return _dfs_lfs_rename(old_dentry->mnt, old_dentry->pathname, new_dentry->pathname); +} +#endif /* !defined(LFS_READONLY) */ + +static int _dfs_lfs_dentry_stat(struct dfs_dentry* dentry, struct stat* st) +{ + RT_ASSERT(dentry != RT_NULL); + RT_ASSERT(dentry->mnt != RT_NULL); + + return _dfs_lfs_stat(dentry->mnt, dentry->pathname, st); +} + +static struct dfs_vnode* _dfs_lfs_lookup(struct dfs_dentry* dentry) +{ + struct dfs_vnode* vnode; + struct stat st; + + if (dentry == RT_NULL || dentry->mnt == RT_NULL || dentry->mnt->data == RT_NULL) + { + return RT_NULL; + } + + if (_dfs_lfs_stat(dentry->mnt, dentry->pathname, &st) != RT_EOK) + { + return RT_NULL; + } + + vnode = dfs_vnode_create(); + if (vnode == RT_NULL) + { + return RT_NULL; + } + + vnode->mnt = dentry->mnt; + vnode->size = st.st_size; + vnode->nlink = 1; + vnode->mode = st.st_mode; + vnode->type = S_ISDIR(st.st_mode) ? FT_DIRECTORY : FT_REGULAR; + vnode->fops = &_dfs_lfs_fops; + vnode->data = RT_NULL; + + return vnode; +} + +static struct dfs_vnode* _dfs_lfs_create_vnode(struct dfs_dentry* dentry, int type, mode_t mode) +{ + struct dfs_vnode* vnode; + + if (dentry == RT_NULL || dentry->mnt == RT_NULL || dentry->mnt->data == RT_NULL) + { + return RT_NULL; + } + + vnode = dfs_vnode_create(); + if (vnode == RT_NULL) + { + return RT_NULL; + } + + vnode->mnt = dentry->mnt; + vnode->size = 0; + vnode->nlink = 1; + vnode->type = type; + vnode->mode = (type == FT_DIRECTORY) ? (S_IFDIR | mode) : (S_IFREG | mode); + vnode->fops = &_dfs_lfs_fops; + vnode->data = RT_NULL; + + return vnode; +} + +static int _dfs_lfs_free_vnode(struct dfs_vnode* vnode) +{ + if (vnode != RT_NULL) + { + vnode->data = RT_NULL; + } + + return RT_EOK; +} + +static const struct dfs_file_ops _dfs_lfs_fops = { + .open = _dfs_lfs_open, + .close = _dfs_lfs_close, + .ioctl = _dfs_lfs_ioctl, + .read = _dfs_lfs_read, +#ifndef LFS_READONLY + .write = _dfs_lfs_write, +#else + .write = RT_NULL, +#endif + .flush = _dfs_lfs_flush, + .lseek = _dfs_lfs_lseek, +#ifndef LFS_READONLY + .truncate = _dfs_lfs_truncate, +#else + .truncate = RT_NULL, +#endif + .getdents = _dfs_lfs_getdents, +}; + +static const struct dfs_filesystem_ops _dfs_lfs_ops = { + .name = "lfs", + .flags = DFS_LFS_FS_FLAGS, + .default_fops = &_dfs_lfs_fops, + .mount = _dfs_lfs_mount, + .umount = _dfs_lfs_unmount, +#ifndef LFS_READONLY + .mkfs = _dfs_lfs_mkfs, +#else + .mkfs = RT_NULL, +#endif +#ifndef LFS_READONLY + .unlink = _dfs_lfs_dentry_unlink, +#else + .unlink = RT_NULL, +#endif +#ifndef LFS_READONLY + .rename = _dfs_lfs_dentry_rename, +#else + .rename = RT_NULL, +#endif + .stat = _dfs_lfs_dentry_stat, + .statfs = _dfs_lfs_statfs, + .lookup = _dfs_lfs_lookup, + .create_vnode = _dfs_lfs_create_vnode, + .free_vnode = _dfs_lfs_free_vnode, +}; + +static struct dfs_filesystem_type _dfs_lfs_type = { + .fs_ops = &_dfs_lfs_ops, +}; +#else static const struct dfs_file_ops _dfs_lfs_fops = { _dfs_lfs_open, _dfs_lfs_close, @@ -1000,42 +1380,43 @@ static const struct dfs_file_ops _dfs_lfs_fops = { #ifndef LFS_READONLY _dfs_lfs_write, #else - NULL, + RT_NULL, #endif _dfs_lfs_flush, _dfs_lfs_lseek, _dfs_lfs_getdents, - // RT_NULL, /* poll interface */ + /* RT_NULL, poll interface */ }; static const struct dfs_filesystem_ops _dfs_lfs_ops = { "lfs", - DFS_FS_FLAG_DEFAULT, + DFS_LFS_FS_FLAGS, &_dfs_lfs_fops, _dfs_lfs_mount, _dfs_lfs_unmount, #ifndef LFS_READONLY _dfs_lfs_mkfs, #else - NULL, + RT_NULL, #endif _dfs_lfs_statfs, #ifndef LFS_READONLY _dfs_lfs_unlink, #else - NULL, + RT_NULL, #endif _dfs_lfs_stat, #ifndef LFS_READONLY _dfs_lfs_rename, #else - NULL, + RT_NULL, #endif }; +#endif /* defined(RT_USING_DFS_V2) */ int dfs_lfs_init(void) { - /* register ram file system */ - return dfs_register(&_dfs_lfs_ops); + /* register littlefs file system */ + return DFS_LFS_REGISTER(); } INIT_COMPONENT_EXPORT(dfs_lfs_init); From 14bd60dd4641ff1b1c7ffa5b29d0a77cb12d62d7 Mon Sep 17 00:00:00 2001 From: wdfk-prog <1425075683@qq.com> Date: Tue, 5 May 2026 20:28:16 +0800 Subject: [PATCH 2/4] fix: bound directory entry name copies avoid copying DFS_PATH_MAX bytes into struct dirent d_name use the actual d_name buffer size when filling littlefs directory entries keep d_namlen consistent with the copied name length after truncation fix stack corruption observed when listing littlefs directories on DFS v2 --- dfs_lfs.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/dfs_lfs.c b/dfs_lfs.c index 75811396..ea4844d7 100644 --- a/dfs_lfs.c +++ b/dfs_lfs.c @@ -1201,9 +1201,21 @@ static int _dfs_lfs_getdents(DFS_LFS_FILE_STRUCT* file, struct dirent* dirp, uin break; } - d->d_namlen = (rt_uint8_t)rt_strlen(info.name); + rt_size_t name_len; + rt_size_t name_max; + + name_max = sizeof(d->d_name); + name_len = rt_strlen(info.name); + + if (name_len >= name_max) + { + name_len = name_max - 1; + } + + d->d_namlen = (rt_uint8_t)name_len; d->d_reclen = (rt_uint16_t)sizeof(struct dirent); - rt_strncpy(d->d_name, info.name, DFS_PATH_MAX); + rt_memcpy(d->d_name, info.name, name_len); + d->d_name[name_len] = '\0'; index++; if (index * sizeof(struct dirent) >= count) From 18a1195711a0223fa323275585b33ef2490df08a Mon Sep 17 00:00:00 2001 From: wdfk-prog <1425075683@qq.com> Date: Tue, 5 May 2026 20:29:54 +0800 Subject: [PATCH 3/4] test[CI][littlefs]: add RT-Thread DFS compile workflow add RT-Thread qemu-vexpress-a9 compile checks for littlefs DFS profiles cover legacy DFS, RT-Thread 5.x DFS v1, DFS v2, and master builds reuse a prepared RT-Thread checkout across profile builds to reduce repeated downloads upload per-profile build logs for CI failure diagnosis --- .github/ci/rtthread-littlefs/build.sh | 695 ++++++++++++++++++++++++++ .github/workflows/rtthread.yml | 109 ++++ 2 files changed, 804 insertions(+) create mode 100644 .github/ci/rtthread-littlefs/build.sh create mode 100644 .github/workflows/rtthread.yml diff --git a/.github/ci/rtthread-littlefs/build.sh b/.github/ci/rtthread-littlefs/build.sh new file mode 100644 index 00000000..28059799 --- /dev/null +++ b/.github/ci/rtthread-littlefs/build.sh @@ -0,0 +1,695 @@ +#!/usr/bin/env sh +set -eu + +usage() { + cat <<'EOF_USAGE' +Usage: build.sh + +Environment variables: + RTTHREAD_REF RT-Thread branch or tag to build against. Default: master + RTTHREAD_BSP BSP path inside rt-thread. Default: bsp/qemu-vexpress-a9 + RTTHREAD_DFS_VERSION + RT-Thread DFS profile to build. Supported values: v1, v2. + Default: v1 + RTT_CC RT-Thread compiler selector. Default: gcc. This CI script + currently supports GNU toolchains only. + RTTHREAD_TOOLCHAIN_PREFIX + GNU toolchain command prefix. Default for RTT_CC=gcc: + arm-none-eabi-. Set to an empty string to use unprefixed + gcc/nm/size commands. + RTTHREAD_CLONE_ATTEMPTS + RT-Thread clone attempts before failing. Default: 3 + RTTHREAD_CLONE_RETRY_DELAY + Seconds to wait between RT-Thread clone attempts. Default: 5 + RTTHREAD_WORKDIR + Temporary RT-Thread work directory. Must be a dedicated + rt-thread-work directory. Default: RUNNER_TEMP or _ci + RTTHREAD_REUSE_WORKDIR + Reuse an existing matching RT-Thread checkout between repeated + invocations. Supported values: 0, 1. Default: 0 + RTTHREAD_ELF ELF file name expected in the BSP directory. Default: + rtthread.elf +EOF_USAGE +} + +fail() { + printf 'error: %s\n' "$*" >&2 + exit 1 +} + +need_cmd() { + command -v "$1" >/dev/null 2>&1 || fail "required command not found: $1" +} + +abs_path() { + case "$1" in + /*) printf '%s\n' "$1" ;; + *) printf '%s/%s\n' "$(pwd)" "$1" ;; + esac +} + +normalize_path() { + path=$1 + + case "$path" in + /*) ;; + *) path=$(abs_path "$path") ;; + esac + + if [ -d "$path" ]; then + (cd "$path" && pwd -P) + return + fi + + dir=$path + suffix= + while [ ! -d "$dir" ]; do + base=${dir##*/} + suffix=${base}${suffix:+/$suffix} + next=${dir%/*} + [ "$next" != "$dir" ] || break + [ -n "$next" ] || next=/ + dir=$next + done + + if [ -d "$dir" ]; then + dir=$(cd "$dir" && pwd -P) + printf '%s%s%s\n' "$dir" "${suffix:+/}" "$suffix" + else + printf '%s\n' "$path" + fi +} + +relative_child_path() { + parent=$1 + child=$2 + parent_abs=$(cd "$parent" && pwd -P) || return 1 + child_abs=$(cd "$child" && pwd -P) || return 1 + + case "$child_abs" in + "$parent_abs") printf '.\n' ;; + "$parent_abs"/*) printf '%s\n' "${child_abs#"$parent_abs"/}" ;; + esac +} + +ensure_safe_workdir() { + work_dir=$1 + root_dir=$2 + + case "$work_dir" in + ''|/) + fail "unsafe RTTHREAD_WORKDIR: $work_dir" + ;; + esac + + work_base=${work_dir##*/} + [ "$work_base" = "rt-thread-work" ] || \ + fail "RTTHREAD_WORKDIR must name a dedicated rt-thread-work directory" + + [ "$work_dir" != "$root_dir" ] || \ + fail "RTTHREAD_WORKDIR must not be the package repository root" + case "$root_dir" in + "$work_dir"/*) + fail "RTTHREAD_WORKDIR must not contain the package repository root" + ;; + esac +} + +ensure_safe_bsp_path() { + bsp_path=$1 + + case "$bsp_path" in + ''|/*) + fail "unsafe RTTHREAD_BSP: $bsp_path" + ;; + .|./*|*/.|*/./*|..|../*|*/..|*/../*|*//*) + fail "RTTHREAD_BSP must be a relative path without . or .. segments" + ;; + esac +} + +ensure_safe_elf_name() { + elf_name=$1 + + case "$elf_name" in + ''|/*|*/*|.|..) + fail "RTTHREAD_ELF must be a file name inside the BSP directory" + ;; + esac +} + +ensure_supported_dfs_version() { + dfs_version=$1 + + case "$dfs_version" in + v1|v2) ;; + *) fail "RTTHREAD_DFS_VERSION must be v1 or v2: $dfs_version" ;; + esac +} + +ensure_removable_workdir() { + work_dir=$1 + marker_file="$work_dir/.rtthread-littlefs-ci-workdir" + + [ ! -e "$work_dir" ] && return + [ -d "$work_dir" ] || \ + fail "RTTHREAD_WORKDIR exists but is not a directory: $work_dir" + [ -f "$marker_file" ] && return + first_entry=$(find "$work_dir" -mindepth 1 -maxdepth 1 \ + -print -quit) || \ + fail "failed to inspect RTTHREAD_WORKDIR: $work_dir" + [ -z "$first_entry" ] && return + + fail "refusing to remove non-empty unmarked RTTHREAD_WORKDIR: $work_dir" +} + +reset_workdir() { + work_dir=$1 + marker_file="$work_dir/.rtthread-littlefs-ci-workdir" + + ensure_removable_workdir "$work_dir" + rm -rf "$work_dir" + mkdir -p "$work_dir" + : > "$marker_file" +} + +kconfig_name() { + case "$1" in + CONFIG_*) printf '%s\n' "$1" ;; + *) printf 'CONFIG_%s\n' "$1" ;; + esac +} + +set_kconfig_symbol() { + config_file=$1 + symbol=$(kconfig_name "$2") + value=$3 + tmp_file="${config_file}.tmp" + + if [ -f "$config_file" ]; then + awk -v symbol="$symbol" -v value="$value" ' + $0 == symbol "=y" || \ + $0 ~ "^" symbol "=" || \ + $0 == "# " symbol " is not set" { + if (!done) { + print symbol "=" value + done = 1 + } + next + } + { print } + END { + if (!done) { + print symbol "=" value + } + } + ' "$config_file" > "$tmp_file" + mv "$tmp_file" "$config_file" + else + printf '%s=%s\n' "$symbol" "$value" > "$config_file" + fi +} + +unset_kconfig_symbol() { + config_file=$1 + symbol=$(kconfig_name "$2") + tmp_file="${config_file}.tmp" + + if [ -f "$config_file" ]; then + awk -v symbol="$symbol" ' + $0 == symbol "=y" || \ + $0 ~ "^" symbol "=" || \ + $0 == "# " symbol " is not set" { + if (!done) { + print "# " symbol " is not set" + done = 1 + } + next + } + { print } + END { + if (!done) { + print "# " symbol " is not set" + } + } + ' "$config_file" > "$tmp_file" + mv "$tmp_file" "$config_file" + else + printf '# %s is not set\n' "$symbol" > "$config_file" + fi +} + +run_scons_pyconfig() { + scons --pyconfig-silent +} + +copy_package() { + source_dir=$1 + package_dir=$2 + exclude_dir=${3:-} + exclude_rel= + archive_file="${package_dir}.tar" + + rm -rf "$package_dir" + mkdir -p "$package_dir" + rm -f "$archive_file" + if [ -n "$exclude_dir" ] && [ -d "$exclude_dir" ]; then + exclude_rel=$(relative_child_path "$source_dir" "$exclude_dir") + fi + + if [ -n "$exclude_rel" ]; then + if ! (cd "$source_dir" && tar \ + --exclude='./.git' \ + --exclude='./_ci' \ + --exclude="./$exclude_rel" \ + --exclude="./$exclude_rel/*" \ + -cf "$archive_file" .); then + rm -f "$archive_file" + fail "failed to archive package sources" + fi + else + if ! (cd "$source_dir" && tar \ + --exclude='./.git' \ + --exclude='./_ci' \ + -cf "$archive_file" .); then + rm -f "$archive_file" + fail "failed to archive package sources" + fi + fi + + if ! tar -xf "$archive_file" -C "$package_dir"; then + rm -f "$archive_file" + fail "failed to extract package sources" + fi + rm -f "$archive_file" +} + +kconfig_tree_has_symbol() { + search_dir=$1 + symbol=$2 + result_file="$search_dir/.rtthread-littlefs-kconfig-symbols.$$" + + [ -d "$search_dir" ] || return 1 + rm -f "$result_file" + if ! find "$search_dir" -name Kconfig -type f -exec awk \ + -v symbol="$symbol" ' + ($1 == "config" || $1 == "menuconfig") && $2 == symbol { + print FILENAME + exit + } + ' {} \; > "$result_file"; then + rm -f "$result_file" + fail "failed to scan Kconfig files under $search_dir" + fi + + if [ -s "$result_file" ]; then + rm -f "$result_file" + return 0 + fi + + rm -f "$result_file" + return 1 +} + +write_packages_kconfig() { + packages_dir=$1 + kconfig_file="$packages_dir/Kconfig" + + kconfig_tree_has_symbol "$packages_dir" PKG_USING_LITTLEFS && return + + cat >> "$kconfig_file" <<'EOF_KCONFIG' + +menu "CI packages" + +config PKG_USING_LITTLEFS + bool "littlefs package" + default n + +endmenu +EOF_KCONFIG +} + +write_packages_sconscript() { + packages_dir=$1 + scons_file="$packages_dir/SConscript" + original_file="$packages_dir/SConscript.ci.orig" + + if [ -f "$original_file" ]; then + rm -f "$scons_file" + elif [ -f "$scons_file" ]; then + mv "$scons_file" "$original_file" + fi + + cat > "$scons_file" <<'EOF_SCONS' +import os + +from building import * + +cwd = GetCurrentDir() +objs = [] + +# CI builds only the package under test. Loading the BSP's original +# packages/SConscript can rescan packages/littlefs and duplicate objects. +script = os.path.join('littlefs', 'SConscript') +if os.path.isfile(os.path.join(cwd, script)): + objs = objs + SConscript(script) + +Return('objs') +EOF_SCONS +} + +write_compile_check_source() { + bsp_dir=$1 + app_dir="$bsp_dir/applications" + + [ -d "$app_dir" ] || fail "BSP applications directory not found: $app_dir" + + cat > "$app_dir/littlefs_compile_check.c" <<'EOF_C' +/* CI-only RT-Thread package integration check. */ +#include + +/* Match the package SConscript compile flag while including the real API. */ +/** + * @brief Select the package-local RT-Thread littlefs configuration header. + */ +#define LFS_CONFIG lfs_config.h +#include "../packages/littlefs/lfs.h" + +/** + * @brief Initialize the RT-Thread DFS littlefs package. + * + * @return 0 on success, otherwise a negative error code. + */ +extern int dfs_lfs_init(void); + +/** + * @brief Verify that package build graph linked littlefs symbols. + * + * @return 0 when required symbols are linked, otherwise -1. + */ +static int littlefs_compile_check(void) +{ + int (* volatile dfs_init)(void) = dfs_lfs_init; + int (* volatile mount)(lfs_t *, const struct lfs_config *) = lfs_mount; + + return (dfs_init != 0 && mount != 0) ? 0 : -1; +} +INIT_APP_EXPORT(littlefs_compile_check); +EOF_C +} + +apply_littlefs_kconfig_profile() { + config_file=$1 + dfs_version=$2 + kconfig_root=$3 + + set_kconfig_symbol "$config_file" RT_USING_COMPONENTS_INIT y + set_kconfig_symbol "$config_file" RT_USING_DEVICE y + set_kconfig_symbol "$config_file" RT_USING_DEVICE_OPS y + set_kconfig_symbol "$config_file" RT_USING_HEAP y + set_kconfig_symbol "$config_file" RT_USING_DFS y + set_kconfig_symbol "$config_file" DFS_USING_POSIX y + set_kconfig_symbol "$config_file" DFS_USING_WORKDIR y + set_kconfig_symbol "$config_file" DFS_FD_MAX 16 + case "$dfs_version" in + v1) + if kconfig_tree_has_symbol "$kconfig_root" RT_USING_DFS_V1; then + set_kconfig_symbol "$config_file" RT_USING_DFS_V1 y + fi + if kconfig_tree_has_symbol "$kconfig_root" RT_USING_DFS_V2; then + unset_kconfig_symbol "$config_file" RT_USING_DFS_V2 + fi + ;; + v2) + kconfig_tree_has_symbol "$kconfig_root" RT_USING_DFS_V2 || \ + fail "RTTHREAD_DFS_VERSION=v2 requires RT_USING_DFS_V2 support" + if kconfig_tree_has_symbol "$kconfig_root" RT_USING_DFS_V1; then + unset_kconfig_symbol "$config_file" RT_USING_DFS_V1 + fi + set_kconfig_symbol "$config_file" RT_USING_DFS_V2 y + unset_kconfig_symbol "$config_file" RT_USING_DFS_ELMFAT + unset_kconfig_symbol "$config_file" RT_USING_DFS_TMPFS + unset_kconfig_symbol "$config_file" RT_USING_DFS_MQUEUE + ;; + esac + set_kconfig_symbol "$config_file" DFS_FILESYSTEMS_MAX 4 + set_kconfig_symbol "$config_file" DFS_FILESYSTEM_TYPES_MAX 4 + set_kconfig_symbol "$config_file" RT_USING_DFS_DEVFS y + set_kconfig_symbol "$config_file" RT_USING_DFS_ROMFS y + set_kconfig_symbol "$config_file" RT_USING_DEVICE_IPC y + set_kconfig_symbol "$config_file" RT_USING_MUTEX y + set_kconfig_symbol "$config_file" RT_USING_MTD_NOR y + set_kconfig_symbol "$config_file" PKG_USING_LITTLEFS y +} + +verify_rtconfig_symbols() { + rtconfig_file=$1 + dfs_version=$2 + kconfig_root=$3 + + grep -q '^#define[[:space:]][[:space:]]*RT_USING_DFS$' "$rtconfig_file" || \ + fail "RT_USING_DFS was not enabled in $rtconfig_file" + case "$dfs_version" in + v1) + if kconfig_tree_has_symbol "$kconfig_root" RT_USING_DFS_V1; then + grep -q '^#define[[:space:]][[:space:]]*RT_USING_DFS_V1$' "$rtconfig_file" || \ + fail "RT_USING_DFS_V1 was not enabled in $rtconfig_file" + else + printf 'RT_USING_DFS_V1 is not available; using legacy DFS v1 profile\n' + fi + if grep -q '^#define[[:space:]][[:space:]]*RT_USING_DFS_V2$' "$rtconfig_file"; then + fail "RT_USING_DFS_V2 is enabled in $rtconfig_file" + fi + ;; + v2) + kconfig_tree_has_symbol "$kconfig_root" RT_USING_DFS_V2 || \ + fail "RTTHREAD_DFS_VERSION=v2 requires RT_USING_DFS_V2 support" + grep -q '^#define[[:space:]][[:space:]]*RT_USING_DFS_V2$' "$rtconfig_file" || \ + fail "RT_USING_DFS_V2 was not enabled in $rtconfig_file" + if grep -q '^#define[[:space:]][[:space:]]*RT_USING_DFS_V1$' "$rtconfig_file"; then + fail "RT_USING_DFS_V1 is enabled in $rtconfig_file" + fi + if grep -q '^#define[[:space:]][[:space:]]*RT_USING_DFS_ELMFAT$' "$rtconfig_file"; then + fail "RT_USING_DFS_ELMFAT is enabled in $rtconfig_file" + fi + if grep -q '^#define[[:space:]][[:space:]]*RT_USING_DFS_TMPFS$' "$rtconfig_file"; then + fail "RT_USING_DFS_TMPFS is enabled in $rtconfig_file" + fi + ;; + esac + grep -q '^#define[[:space:]][[:space:]]*RT_USING_MTD_NOR$' "$rtconfig_file" || \ + fail "RT_USING_MTD_NOR was not enabled in $rtconfig_file" + grep -q '^#define[[:space:]][[:space:]]*RT_USING_HEAP$' "$rtconfig_file" || \ + fail "RT_USING_HEAP was not enabled in $rtconfig_file" + grep -q '^#define[[:space:]][[:space:]]*RT_USING_MUTEX$' "$rtconfig_file" || \ + fail "RT_USING_MUTEX was not enabled in $rtconfig_file" + grep -q '^#define[[:space:]][[:space:]]*PKG_USING_LITTLEFS$' "$rtconfig_file" || \ + fail "PKG_USING_LITTLEFS was not enabled in $rtconfig_file" +} + +check_nm_symbol() { + nm_cmd=$1 + elf_file=$2 + symbol=$3 + + "$nm_cmd" "$elf_file" | awk -v symbol="$symbol" ' + $NF == symbol { + found = 1 + } + END { + exit !found + } + ' +} + +gnu_toolchain_prefix() { + case "$1" in + gcc) + if [ "${RTTHREAD_TOOLCHAIN_PREFIX+x}" = x ]; then + printf '%s\n' "$RTTHREAD_TOOLCHAIN_PREFIX" + else + printf '%s\n' 'arm-none-eabi-' + fi + ;; + *) + fail "unsupported RTT_CC for this CI script: $1" + ;; + esac +} + +clone_rtthread() { + rtthread_ref=$1 + rtthread_dir=$2 + attempts=${RTTHREAD_CLONE_ATTEMPTS:-3} + retry_delay=${RTTHREAD_CLONE_RETRY_DELAY:-5} + attempt=1 + + case "$attempts" in + ''|*[!0-9]*) + fail "RTTHREAD_CLONE_ATTEMPTS must be a positive integer" + ;; + esac + case "$retry_delay" in + ''|*[!0-9]*) + fail "RTTHREAD_CLONE_RETRY_DELAY must be a non-negative integer" + ;; + esac + [ "$attempts" -gt 0 ] || \ + fail "RTTHREAD_CLONE_ATTEMPTS must be a positive integer" + + while [ "$attempt" -le "$attempts" ]; do + printf 'cloning RT-Thread branch/tag %s (attempt %s/%s)\n' \ + "$rtthread_ref" "$attempt" "$attempts" + if git clone --depth 1 --branch "$rtthread_ref" \ + https://github.com/RT-Thread/rt-thread.git "$rtthread_dir"; then + return + fi + + rm -rf "$rtthread_dir" + [ "$attempt" -lt "$attempts" ] || break + printf 'warning: RT-Thread clone failed; retrying in %s seconds\n' \ + "$retry_delay" >&2 + sleep "$retry_delay" + attempt=$((attempt + 1)) + done + + fail "failed to clone RT-Thread branch/tag $rtthread_ref after $attempts attempts" +} + + +rtthread_checkout_matches() { + rtthread_ref=$1 + rtthread_dir=$2 + + [ -d "$rtthread_dir/.git" ] || return 1 + git -C "$rtthread_dir" rev-parse --verify HEAD >/dev/null 2>&1 || return 1 + + if git -C "$rtthread_dir" rev-parse --verify "refs/tags/$rtthread_ref^{commit}" >/dev/null 2>&1; then + expected=$(git -C "$rtthread_dir" rev-parse "refs/tags/$rtthread_ref^{commit}") || return 1 + elif git -C "$rtthread_dir" rev-parse --verify "refs/remotes/origin/$rtthread_ref^{commit}" >/dev/null 2>&1; then + expected=$(git -C "$rtthread_dir" rev-parse "refs/remotes/origin/$rtthread_ref^{commit}") || return 1 + else + return 1 + fi + + actual=$(git -C "$rtthread_dir" rev-parse HEAD) || return 1 + [ "$actual" = "$expected" ] +} + +reset_rtthread_checkout() { + rtthread_dir=$1 + + git -C "$rtthread_dir" reset --hard + git -C "$rtthread_dir" clean -fdx +} + +prepare_rtthread_checkout() { + rtthread_ref=$1 + rtthread_work=$2 + rtthread_dir=$3 + + case "${RTTHREAD_REUSE_WORKDIR:-0}" in + 1|yes|true|TRUE|on|ON) + ensure_removable_workdir "$rtthread_work" + mkdir -p "$rtthread_work" + : > "$rtthread_work/.rtthread-littlefs-ci-workdir" + if rtthread_checkout_matches "$rtthread_ref" "$rtthread_dir"; then + printf 'reusing RT-Thread checkout for branch/tag %s\n' "$rtthread_ref" + reset_rtthread_checkout "$rtthread_dir" + return + fi + reset_workdir "$rtthread_work" + clone_rtthread "$rtthread_ref" "$rtthread_dir" + ;; + 0|no|false|FALSE|off|OFF) + reset_workdir "$rtthread_work" + clone_rtthread "$rtthread_ref" "$rtthread_dir" + ;; + *) + fail "RTTHREAD_REUSE_WORKDIR must be 0 or 1: ${RTTHREAD_REUSE_WORKDIR:-}" + ;; + esac +} + +verify_symbols() { + bsp_dir=$1 + nm_cmd=$2 + size_cmd=$3 + elf_name=$4 + elf_file="$bsp_dir/$elf_name" + + [ -f "$elf_file" ] || fail "expected ELF output not found: $elf_file" + need_cmd "$nm_cmd" + + check_nm_symbol "$nm_cmd" "$elf_file" dfs_lfs_init || \ + fail "dfs_lfs_init symbol not found in $elf_file" + check_nm_symbol "$nm_cmd" "$elf_file" lfs_mount || \ + fail "lfs_mount symbol not found in $elf_file" + + "$size_cmd" "$elf_file" || true + printf 'verified littlefs symbols in %s\n' "$elf_file" +} + +if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then + usage + exit 0 +fi + +need_cmd git +need_cmd tar +need_cmd awk +need_cmd dirname +need_cmd find +need_cmd getconf +need_cmd grep +need_cmd scons +need_cmd sleep + +repo_root=$(pwd -P) +rtthread_ref=${RTTHREAD_REF:-master} +rtthread_bsp=${RTTHREAD_BSP:-bsp/qemu-vexpress-a9} +rtthread_dfs_version=${RTTHREAD_DFS_VERSION:-v1} +export RTT_CC=${RTT_CC:-gcc} +toolchain_prefix=$(gnu_toolchain_prefix "$RTT_CC") +cc_cmd=${toolchain_prefix}gcc +nm_cmd=${NM:-${toolchain_prefix}nm} +size_cmd=${SIZE:-${toolchain_prefix}size} +rtthread_elf=${RTTHREAD_ELF:-rtthread.elf} +need_cmd "$cc_cmd" +need_cmd "$nm_cmd" +need_cmd "$size_cmd" +ensure_safe_bsp_path "$rtthread_bsp" +ensure_safe_elf_name "$rtthread_elf" +ensure_supported_dfs_version "$rtthread_dfs_version" +rtthread_work=$(normalize_path "${RTTHREAD_WORKDIR:-${RUNNER_TEMP:-$repo_root/_ci}/rt-thread-work}") +ensure_safe_workdir "$rtthread_work" "$repo_root" +rtthread_dir="$rtthread_work/rt-thread" +bsp_dir="$rtthread_dir/$rtthread_bsp" +package_dir="$bsp_dir/packages/littlefs" + +prepare_rtthread_checkout "$rtthread_ref" "$rtthread_work" "$rtthread_dir" + +[ -d "$bsp_dir" ] || fail "RT-Thread BSP not found: $rtthread_bsp" + +copy_package "$repo_root" "$package_dir" "$rtthread_work" +write_packages_kconfig "$bsp_dir/packages" +write_packages_sconscript "$bsp_dir/packages" +write_compile_check_source "$bsp_dir" + +export RTT_ROOT="$rtthread_dir" +RTT_EXEC_PATH=${RTT_EXEC_PATH:-$(dirname "$(command -v "$cc_cmd")")} +export RTT_EXEC_PATH +export PYTHONPATH="$rtthread_dir/tools${PYTHONPATH:+:$PYTHONPATH}" + +cd "$bsp_dir" +run_scons_pyconfig + +apply_littlefs_kconfig_profile .config "$rtthread_dfs_version" "$rtthread_dir" +run_scons_pyconfig + +grep -E '^(CONFIG_)?(PKG_USING_LITTLEFS|RT_USING_DFS|DFS_|RT_USING_MTD_NOR|RT_USING_DEVICE|RT_USING_HEAP|RT_USING_MUTEX)' \ + .config rtconfig.h rtconfig.py || true +verify_rtconfig_symbols rtconfig.h "$rtthread_dfs_version" "$rtthread_dir" + +scons -j"$(getconf _NPROCESSORS_ONLN)" +verify_symbols "$bsp_dir" "$nm_cmd" "$size_cmd" "$rtthread_elf" diff --git a/.github/workflows/rtthread.yml b/.github/workflows/rtthread.yml new file mode 100644 index 00000000..cab318b5 --- /dev/null +++ b/.github/workflows/rtthread.yml @@ -0,0 +1,109 @@ +name: rtthread + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash -euo pipefail {0} + +jobs: + compile-littlefs: + name: RT-Thread DFS compile + runs-on: ubuntu-22.04 + timeout-minutes: 180 + + env: + RTTHREAD_ELF: rtthread.elf + RTT_CC: gcc + RTTHREAD_REUSE_WORKDIR: 1 + RTTHREAD_BUILD_PROFILES: | + v4.1.1 v1 bsp/qemu-vexpress-a9 + v5.0.0 v1 bsp/qemu-vexpress-a9 + v5.0.1 v1 bsp/qemu-vexpress-a9 + v5.0.1 v2 bsp/qemu-vexpress-a9 + v5.0.2 v1 bsp/qemu-vexpress-a9 + v5.0.2 v2 bsp/qemu-vexpress-a9 + master v1 bsp/qemu-vexpress-a9 + master v2 bsp/qemu-vexpress-a9 + + steps: + - name: Checkout littlefs package + uses: actions/checkout@v4 + + - name: Install build dependencies + run: | + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends \ + ca-certificates \ + git \ + make \ + gcc-arm-none-eabi \ + libnewlib-arm-none-eabi \ + libstdc++-arm-none-eabi-newlib \ + scons \ + python3 \ + python3-pip + python3 -m pip install --user kconfiglib + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + arm-none-eabi-gcc --version + scons --version + python3 --version + + - name: Compile littlefs against RT-Thread profiles + run: | + mkdir -p _ci/logs + printf '%s\n' "$RTTHREAD_BUILD_PROFILES" > _ci/rtthread-build-profiles.txt + + failed_profiles_file=_ci/failed-rtthread-profiles.txt + : > "$failed_profiles_file" + while read -r rtthread_ref dfs_version bsp; do + [ -n "${rtthread_ref:-}" ] || continue + case "$rtthread_ref" in + \#*) continue ;; + esac + + log_ref=${rtthread_ref//\//-} + log_file="_ci/logs/rtthread-${log_ref}-${dfs_version}.log" + printf 'building RT-Thread %s DFS %s with BSP %s\n' \ + "$rtthread_ref" "$dfs_version" "$bsp" + + if RTTHREAD_REF="$rtthread_ref" \ + RTTHREAD_DFS_VERSION="$dfs_version" \ + RTTHREAD_BSP="$bsp" \ + sh .github/ci/rtthread-littlefs/build.sh 2>&1 | tee "$log_file"; then + printf 'profile passed: %s %s %s\n' \ + "$rtthread_ref" "$dfs_version" "$bsp" + else + printf 'profile failed: %s %s %s\n' \ + "$rtthread_ref" "$dfs_version" "$bsp" >&2 + printf '%s %s %s\n' \ + "$rtthread_ref" "$dfs_version" "$bsp" >> "$failed_profiles_file" + fi + done < _ci/rtthread-build-profiles.txt + + if [ -s "$failed_profiles_file" ]; then + printf 'failed RT-Thread profiles:\n' >&2 + cat "$failed_profiles_file" >&2 + exit 1 + fi + + - name: Upload RT-Thread compile logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: rtthread-compile-logs + path: | + _ci/logs/*.log + _ci/rtthread-build-profiles.txt + _ci/failed-rtthread-profiles.txt + if-no-files-found: warn From 2e767b1ad88f7bcf9cc9900169ff6afe891b01f8 Mon Sep 17 00:00:00 2001 From: wdfk-prog <1425075683@qq.com> Date: Thu, 7 May 2026 17:45:40 +0800 Subject: [PATCH 4/4] test[CI][littlefs]: add RT-Thread QEMU smoke coverage - add QEMU littlefs smoke sources for RT-Thread BSP validation - split RT-Thread tag and master checks into separate GitHub Actions matrix jobs - add RT-Thread source archive/cache handling to reduce repeated clone cost - run compile and smoke verification against qemu-vexpress-a9 DFS v1/v2 profiles --- .../actions/setup-rtthread-build/action.yml | 218 ++++++++++++ .github/ci/rtthread-littlefs/build.sh | 238 ++++++++++--- .github/ci/rtthread-littlefs/requirements.txt | 2 + .../smoke/littlefs_compile_check.c | 30 ++ .../rtthread-littlefs/smoke/littlefs_smoke.c | 320 ++++++++++++++++++ .../ci/rtthread-littlefs/smoke/qemu_lfs_mtd.c | 219 ++++++++++++ .github/workflows/rtthread.yml | 259 ++++++++++---- 7 files changed, 1172 insertions(+), 114 deletions(-) create mode 100644 .github/actions/setup-rtthread-build/action.yml create mode 100644 .github/ci/rtthread-littlefs/requirements.txt create mode 100644 .github/ci/rtthread-littlefs/smoke/littlefs_compile_check.c create mode 100644 .github/ci/rtthread-littlefs/smoke/littlefs_smoke.c create mode 100644 .github/ci/rtthread-littlefs/smoke/qemu_lfs_mtd.c diff --git a/.github/actions/setup-rtthread-build/action.yml b/.github/actions/setup-rtthread-build/action.yml new file mode 100644 index 00000000..9fc41c50 --- /dev/null +++ b/.github/actions/setup-rtthread-build/action.yml @@ -0,0 +1,218 @@ +name: Set up RT-Thread littlefs build +description: Install shared RT-Thread littlefs CI build dependencies and export build environment. + +inputs: + python-version: + description: Python version used by RT-Thread SCons tooling. + required: false + default: '3.10' + requirements-path: + description: Path to the pip requirements file for RT-Thread littlefs CI. + required: false + default: .github/ci/rtthread-littlefs/requirements.txt + arm-gcc-version: + description: ARM GCC toolchain version. + required: false + default: 10.3-2021.10 + arm-gcc-archive: + description: ARM GCC toolchain archive name. + required: false + default: gcc-arm-none-eabi-10.3-2021.10-x86_64-linux.tar.bz2 + arm-gcc-md5: + description: Expected MD5 checksum for the ARM GCC toolchain archive. + required: false + default: 2383e4eb4ea23f248d33adc70dc3227e + arm-gcc-dir: + description: Installation directory for the cached ARM GCC toolchain. + required: false + default: /opt/gcc-arm-none-eabi + rtthread-ref: + description: RT-Thread git ref used by the littlefs build. + required: true + rtthread-dfs-version: + description: RT-Thread DFS profile version used by the littlefs build. + required: true + rtthread-bsp: + description: RT-Thread BSP path used by the littlefs build. + required: true + rtthread-elf: + description: Expected RT-Thread ELF output name. + required: false + default: rtthread.elf + rtthread-reuse-workdir: + description: Whether the build script may reuse an existing RT-Thread workdir. + required: false + default: '0' + rtthread-run-smoke: + description: Whether to run the littlefs QEMU smoke test. + required: false + default: '1' + rtthread-qemu-timeout: + description: QEMU smoke test timeout in seconds. + required: false + default: '120' + rtthread-source-cache-dir: + description: Optional RT-Thread source cache directory for immutable refs. + required: false + default: '' + rtthread-source-archive: + description: Optional RT-Thread source archive for prepacked refs such as master. + required: false + default: '' + rtthread-source-cache-read: + description: Whether the build script may restore RT-Thread source from cache. + required: false + default: '1' + rtt-cc: + description: RT-Thread compiler selector. + required: false + default: gcc + +runs: + using: composite + steps: + - name: Export RT-Thread littlefs build environment + shell: bash + env: + ARM_GCC_DIR: ${{ inputs.arm-gcc-dir }} + ARM_GCC_VERSION: ${{ inputs.arm-gcc-version }} + ARM_GCC_ARCHIVE: ${{ inputs.arm-gcc-archive }} + ARM_GCC_MD5: ${{ inputs.arm-gcc-md5 }} + RTT_CC: ${{ inputs.rtt-cc }} + RTTHREAD_REF: ${{ inputs.rtthread-ref }} + RTTHREAD_DFS_VERSION: ${{ inputs.rtthread-dfs-version }} + RTTHREAD_BSP: ${{ inputs.rtthread-bsp }} + RTTHREAD_ELF: ${{ inputs.rtthread-elf }} + RTTHREAD_REUSE_WORKDIR: ${{ inputs.rtthread-reuse-workdir }} + RTTHREAD_RUN_SMOKE: ${{ inputs.rtthread-run-smoke }} + RTTHREAD_QEMU_TIMEOUT: ${{ inputs.rtthread-qemu-timeout }} + RTTHREAD_SOURCE_CACHE_DIR: ${{ inputs.rtthread-source-cache-dir }} + RTTHREAD_SOURCE_ARCHIVE: ${{ inputs.rtthread-source-archive }} + RTTHREAD_SOURCE_CACHE_READ: ${{ inputs.rtthread-source-cache-read }} + run: | + set -euo pipefail + { + printf 'ARM_GCC_DIR=%s\n' "$ARM_GCC_DIR" + printf 'ARM_GCC_VERSION=%s\n' "$ARM_GCC_VERSION" + printf 'ARM_GCC_ARCHIVE=%s\n' "$ARM_GCC_ARCHIVE" + printf 'ARM_GCC_MD5=%s\n' "$ARM_GCC_MD5" + printf 'RTT_CC=%s\n' "$RTT_CC" + printf 'RTT_EXEC_PATH=%s/bin\n' "$ARM_GCC_DIR" + printf 'RTTHREAD_REF=%s\n' "$RTTHREAD_REF" + printf 'RTTHREAD_DFS_VERSION=%s\n' "$RTTHREAD_DFS_VERSION" + printf 'RTTHREAD_BSP=%s\n' "$RTTHREAD_BSP" + printf 'RTTHREAD_ELF=%s\n' "$RTTHREAD_ELF" + printf 'RTTHREAD_REUSE_WORKDIR=%s\n' "$RTTHREAD_REUSE_WORKDIR" + printf 'RTTHREAD_RUN_SMOKE=%s\n' "$RTTHREAD_RUN_SMOKE" + printf 'RTTHREAD_QEMU_TIMEOUT=%s\n' "$RTTHREAD_QEMU_TIMEOUT" + printf 'RTTHREAD_SOURCE_CACHE_READ=%s\n' "$RTTHREAD_SOURCE_CACHE_READ" + if [ -n "$RTTHREAD_SOURCE_CACHE_DIR" ]; then + printf 'RTTHREAD_SOURCE_CACHE_DIR=%s\n' "$RTTHREAD_SOURCE_CACHE_DIR" + fi + if [ -n "$RTTHREAD_SOURCE_ARCHIVE" ]; then + printf 'RTTHREAD_SOURCE_ARCHIVE=%s\n' "$RTTHREAD_SOURCE_ARCHIVE" + fi + } >> "$GITHUB_ENV" + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version }} + cache: pip + cache-dependency-path: ${{ inputs.requirements-path }} + + - name: Prepare ARM GCC cache directory + shell: bash + env: + ARM_GCC_DIR: ${{ inputs.arm-gcc-dir }} + run: | + set -euo pipefail + sudo mkdir -p "$ARM_GCC_DIR" + sudo chown -R "$USER:$(id -gn)" "$ARM_GCC_DIR" + + - name: Cache ARM GCC toolchain + id: cache-arm-gcc + uses: actions/cache@v4 + with: + path: ${{ inputs.arm-gcc-dir }} + key: ${{ runner.os }}-arm-gcc-${{ inputs.arm-gcc-version }}-v1 + + - name: Install build dependencies + shell: bash + env: + REQUIREMENTS_PATH: ${{ inputs.requirements-path }} + run: | + timeout 25m bash -euo pipefail <<'INSTALL_DEPS' + export DEBIAN_FRONTEND=noninteractive + + sudo apt-get update -qq \ + -o Acquire::Retries=5 \ + -o Acquire::http::Timeout=30 \ + -o Acquire::https::Timeout=30 + + timeout 20m sudo apt-get install -y --no-install-recommends \ + -o Acquire::Retries=5 \ + -o Acquire::http::Timeout=30 \ + -o Acquire::https::Timeout=30 \ + bzip2 \ + ca-certificates \ + git \ + make \ + qemu-system-arm \ + wget + + python -m pip install --user -r "$REQUIREMENTS_PATH" + export PATH="$HOME/.local/bin:$PATH" + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + scons --version + python --version + INSTALL_DEPS + + - name: Install cached ARM GCC toolchain + if: steps.cache-arm-gcc.outputs.cache-hit != 'true' + shell: bash + env: + ARM_GCC_ARCHIVE: ${{ inputs.arm-gcc-archive }} + ARM_GCC_DIR: ${{ inputs.arm-gcc-dir }} + ARM_GCC_MD5: ${{ inputs.arm-gcc-md5 }} + ARM_GCC_VERSION: ${{ inputs.arm-gcc-version }} + run: | + timeout 25m bash -euo pipefail <<'INSTALL_ARM_GCC' + toolchain_tmp=$(mktemp -d) + trap 'rm -rf "$toolchain_tmp"' EXIT + + download_url="https://developer.arm.com/-/media/Files/downloads/gnu-rm/${ARM_GCC_VERSION}/${ARM_GCC_ARCHIVE}" + archive_path="$toolchain_tmp/$ARM_GCC_ARCHIVE" + + for attempt in 1 2 3; do + printf 'downloading ARM GCC toolchain attempt %s/3\n' "$attempt" + if timeout 20m wget --progress=dot:giga --tries=3 --timeout=30 \ + -O "$archive_path" "$download_url"; then + break + fi + + if [ "$attempt" = 3 ]; then + printf 'failed to download ARM GCC toolchain\n' >&2 + exit 1 + fi + sleep 10 + done + + printf '%s %s\n' "$ARM_GCC_MD5" "$archive_path" | md5sum -c - + tar -xjf "$archive_path" -C "$toolchain_tmp" + + find "$ARM_GCC_DIR" -mindepth 1 -maxdepth 1 -exec rm -rf {} + + cp -a "$toolchain_tmp/gcc-arm-none-eabi-${ARM_GCC_VERSION}/." \ + "$ARM_GCC_DIR/" + test -x "$ARM_GCC_DIR/bin/arm-none-eabi-gcc" + INSTALL_ARM_GCC + + - name: Setup ARM GCC environment + shell: bash + env: + ARM_GCC_DIR: ${{ inputs.arm-gcc-dir }} + run: | + set -euo pipefail + echo "$ARM_GCC_DIR/bin" >> "$GITHUB_PATH" + echo "RTT_EXEC_PATH=$ARM_GCC_DIR/bin" >> "$GITHUB_ENV" + "$ARM_GCC_DIR/bin/arm-none-eabi-gcc" --version diff --git a/.github/ci/rtthread-littlefs/build.sh b/.github/ci/rtthread-littlefs/build.sh index 28059799..5c7ad2c0 100644 --- a/.github/ci/rtthread-littlefs/build.sh +++ b/.github/ci/rtthread-littlefs/build.sh @@ -27,8 +27,25 @@ Environment variables: RTTHREAD_REUSE_WORKDIR Reuse an existing matching RT-Thread checkout between repeated invocations. Supported values: 0, 1. Default: 0 + RTTHREAD_SOURCE_CACHE_DIR + Optional directory containing clean RT-Thread source tarballs. + When set, the script restores a clean cached checkout before + cloning and writes a fresh tarball after cloning. + RTTHREAD_SOURCE_CACHE_READ + Restore from RTTHREAD_SOURCE_CACHE_DIR when available. Use 0 + for moving branches such as master to avoid stale source. + Supported values: 0, 1. Default: 1 + RTTHREAD_SOURCE_ARCHIVE + Optional clean RT-Thread source tarball to extract before + restoring from cache or cloning. This is intended for moving + refs prepared as same-run workflow artifacts. RTTHREAD_ELF ELF file name expected in the BSP directory. Default: rtthread.elf + RTTHREAD_RUN_SMOKE + Run the QEMU littlefs smoke test after a successful build. + Supported values: 0, 1. Default: 0 + RTTHREAD_QEMU_TIMEOUT + Seconds to wait for the QEMU smoke test result. Default: 120 EOF_USAGE } @@ -358,44 +375,42 @@ Return('objs') EOF_SCONS } -write_compile_check_source() { +require_ci_source() { + source_file=$1 + + [ -f "$source_file" ] || fail "CI source not found: $source_file" +} + +copy_ci_application_source() { + source_dir=$1 + app_dir=$2 + source_name=$3 + source_file="$source_dir/$source_name" + + require_ci_source "$source_file" + cp "$source_file" "$app_dir/$source_name" +} + +install_compile_check_source() { bsp_dir=$1 + source_dir=$2 app_dir="$bsp_dir/applications" [ -d "$app_dir" ] || fail "BSP applications directory not found: $app_dir" - cat > "$app_dir/littlefs_compile_check.c" <<'EOF_C' -/* CI-only RT-Thread package integration check. */ -#include - -/* Match the package SConscript compile flag while including the real API. */ -/** - * @brief Select the package-local RT-Thread littlefs configuration header. - */ -#define LFS_CONFIG lfs_config.h -#include "../packages/littlefs/lfs.h" + copy_ci_application_source "$source_dir" "$app_dir" \ + littlefs_compile_check.c +} -/** - * @brief Initialize the RT-Thread DFS littlefs package. - * - * @return 0 on success, otherwise a negative error code. - */ -extern int dfs_lfs_init(void); +install_smoke_sources() { + bsp_dir=$1 + source_dir=$2 + app_dir="$bsp_dir/applications" -/** - * @brief Verify that package build graph linked littlefs symbols. - * - * @return 0 when required symbols are linked, otherwise -1. - */ -static int littlefs_compile_check(void) -{ - int (* volatile dfs_init)(void) = dfs_lfs_init; - int (* volatile mount)(lfs_t *, const struct lfs_config *) = lfs_mount; + [ -d "$app_dir" ] || fail "BSP applications directory not found: $app_dir" - return (dfs_init != 0 && mount != 0) ? 0 : -1; -} -INIT_APP_EXPORT(littlefs_compile_check); -EOF_C + copy_ci_application_source "$source_dir" "$app_dir" qemu_lfs_mtd.c + copy_ci_application_source "$source_dir" "$app_dir" littlefs_smoke.c } apply_littlefs_kconfig_profile() { @@ -411,6 +426,8 @@ apply_littlefs_kconfig_profile() { set_kconfig_symbol "$config_file" DFS_USING_POSIX y set_kconfig_symbol "$config_file" DFS_USING_WORKDIR y set_kconfig_symbol "$config_file" DFS_FD_MAX 16 + # Keep QEMU smoke tests independent from BSP SD-card FAT auto-mount. + unset_kconfig_symbol "$config_file" RT_USING_DFS_ELMFAT case "$dfs_version" in v1) if kconfig_tree_has_symbol "$kconfig_root" RT_USING_DFS_V1; then @@ -427,15 +444,14 @@ apply_littlefs_kconfig_profile() { unset_kconfig_symbol "$config_file" RT_USING_DFS_V1 fi set_kconfig_symbol "$config_file" RT_USING_DFS_V2 y - unset_kconfig_symbol "$config_file" RT_USING_DFS_ELMFAT - unset_kconfig_symbol "$config_file" RT_USING_DFS_TMPFS unset_kconfig_symbol "$config_file" RT_USING_DFS_MQUEUE ;; esac - set_kconfig_symbol "$config_file" DFS_FILESYSTEMS_MAX 4 - set_kconfig_symbol "$config_file" DFS_FILESYSTEM_TYPES_MAX 4 + set_kconfig_symbol "$config_file" DFS_FILESYSTEMS_MAX 8 + set_kconfig_symbol "$config_file" DFS_FILESYSTEM_TYPES_MAX 8 set_kconfig_symbol "$config_file" RT_USING_DFS_DEVFS y set_kconfig_symbol "$config_file" RT_USING_DFS_ROMFS y + set_kconfig_symbol "$config_file" RT_USING_DFS_TMPFS y set_kconfig_symbol "$config_file" RT_USING_DEVICE_IPC y set_kconfig_symbol "$config_file" RT_USING_MUTEX y set_kconfig_symbol "$config_file" RT_USING_MTD_NOR y @@ -469,12 +485,6 @@ verify_rtconfig_symbols() { if grep -q '^#define[[:space:]][[:space:]]*RT_USING_DFS_V1$' "$rtconfig_file"; then fail "RT_USING_DFS_V1 is enabled in $rtconfig_file" fi - if grep -q '^#define[[:space:]][[:space:]]*RT_USING_DFS_ELMFAT$' "$rtconfig_file"; then - fail "RT_USING_DFS_ELMFAT is enabled in $rtconfig_file" - fi - if grep -q '^#define[[:space:]][[:space:]]*RT_USING_DFS_TMPFS$' "$rtconfig_file"; then - fail "RT_USING_DFS_TMPFS is enabled in $rtconfig_file" - fi ;; esac grep -q '^#define[[:space:]][[:space:]]*RT_USING_MTD_NOR$' "$rtconfig_file" || \ @@ -502,6 +512,13 @@ check_nm_symbol() { ' } +safe_log_fragment() { + printf '%s\n' "$1" | awk '{ + gsub(/[^A-Za-z0-9_.-]/, "-") + print + }' +} + gnu_toolchain_prefix() { case "$1" in gcc) @@ -583,6 +600,101 @@ reset_rtthread_checkout() { git -C "$rtthread_dir" clean -fdx } +rtthread_source_cache_file() { + rtthread_ref=$1 + cache_dir=${RTTHREAD_SOURCE_CACHE_DIR:-} + + [ -n "$cache_dir" ] || return 1 + cache_ref=$(safe_log_fragment "$rtthread_ref") + mkdir -p "$cache_dir" + printf '%s/rt-thread-%s.tar\n' "$cache_dir" "$cache_ref" +} + +restore_rtthread_source_archive() { + rtthread_ref=$1 + rtthread_work=$2 + rtthread_dir=$3 + source_archive=${RTTHREAD_SOURCE_ARCHIVE:-} + + [ -n "$source_archive" ] || return 1 + [ -f "$source_archive" ] || \ + fail "RTTHREAD_SOURCE_ARCHIVE not found: $source_archive" + + printf 'restoring clean RT-Thread source artifact: %s\n' \ + "$source_archive" + rm -rf "$rtthread_dir" + if ! tar -xf "$source_archive" -C "$rtthread_work"; then + rm -rf "$rtthread_dir" + fail "failed to extract RTTHREAD_SOURCE_ARCHIVE: $source_archive" + fi + + if ! rtthread_checkout_matches "$rtthread_ref" "$rtthread_dir"; then + rm -rf "$rtthread_dir" + fail "RTTHREAD_SOURCE_ARCHIVE did not match $rtthread_ref" + fi + + reset_rtthread_checkout "$rtthread_dir" + return 0 +} + +restore_rtthread_source_cache() { + rtthread_ref=$1 + rtthread_work=$2 + rtthread_dir=$3 + + case "${RTTHREAD_SOURCE_CACHE_READ:-1}" in + 1|yes|true|TRUE|on|ON) ;; + 0|no|false|FALSE|off|OFF) + printf 'RT-Thread source cache restore disabled for %s\n' \ + "$rtthread_ref" + return 1 + ;; + *) + fail "RTTHREAD_SOURCE_CACHE_READ must be 0 or 1: ${RTTHREAD_SOURCE_CACHE_READ:-}" + ;; + esac + + cache_file=$(rtthread_source_cache_file "$rtthread_ref") || return 1 + [ -f "$cache_file" ] || return 1 + + printf 'restoring clean RT-Thread source cache: %s\n' "$cache_file" + rm -rf "$rtthread_dir" + if ! tar -xf "$cache_file" -C "$rtthread_work"; then + printf 'warning: failed to extract RT-Thread source cache\n' >&2 + rm -rf "$rtthread_dir" + return 1 + fi + + if ! rtthread_checkout_matches "$rtthread_ref" "$rtthread_dir"; then + printf 'warning: RT-Thread source cache did not match %s\n' \ + "$rtthread_ref" >&2 + rm -rf "$rtthread_dir" + return 1 + fi + + reset_rtthread_checkout "$rtthread_dir" + return 0 +} + +save_rtthread_source_cache() { + rtthread_ref=$1 + rtthread_work=$2 + rtthread_dir=$3 + + cache_file=$(rtthread_source_cache_file "$rtthread_ref") || return 0 + [ -d "$rtthread_dir/.git" ] || return 0 + + cache_tmp="${cache_file}.$$" + rm -f "$cache_tmp" + printf 'saving clean RT-Thread source cache: %s\n' "$cache_file" + if tar -cf "$cache_tmp" -C "$rtthread_work" rt-thread; then + mv "$cache_tmp" "$cache_file" + else + rm -f "$cache_tmp" + printf 'warning: failed to save RT-Thread source cache\n' >&2 + fi +} + prepare_rtthread_checkout() { rtthread_ref=$1 rtthread_work=$2 @@ -594,16 +706,37 @@ prepare_rtthread_checkout() { mkdir -p "$rtthread_work" : > "$rtthread_work/.rtthread-littlefs-ci-workdir" if rtthread_checkout_matches "$rtthread_ref" "$rtthread_dir"; then - printf 'reusing RT-Thread checkout for branch/tag %s\n' "$rtthread_ref" + printf 'reusing RT-Thread checkout for branch/tag %s\n' \ + "$rtthread_ref" reset_rtthread_checkout "$rtthread_dir" return fi reset_workdir "$rtthread_work" + if restore_rtthread_source_archive "$rtthread_ref" \ + "$rtthread_work" "$rtthread_dir"; then + return + fi + if restore_rtthread_source_cache "$rtthread_ref" "$rtthread_work" \ + "$rtthread_dir"; then + return + fi clone_rtthread "$rtthread_ref" "$rtthread_dir" + save_rtthread_source_cache "$rtthread_ref" "$rtthread_work" \ + "$rtthread_dir" ;; 0|no|false|FALSE|off|OFF) reset_workdir "$rtthread_work" + if restore_rtthread_source_archive "$rtthread_ref" \ + "$rtthread_work" "$rtthread_dir"; then + return + fi + if restore_rtthread_source_cache "$rtthread_ref" "$rtthread_work" \ + "$rtthread_dir"; then + return + fi clone_rtthread "$rtthread_ref" "$rtthread_dir" + save_rtthread_source_cache "$rtthread_ref" "$rtthread_work" \ + "$rtthread_dir" ;; *) fail "RTTHREAD_REUSE_WORKDIR must be 0 or 1: ${RTTHREAD_REUSE_WORKDIR:-}" @@ -638,6 +771,7 @@ fi need_cmd git need_cmd tar need_cmd awk +need_cmd cp need_cmd dirname need_cmd find need_cmd getconf @@ -645,6 +779,9 @@ need_cmd grep need_cmd scons need_cmd sleep +script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd -P) || \ + fail "failed to resolve CI script directory" +ci_source_dir="$script_dir/smoke" repo_root=$(pwd -P) rtthread_ref=${RTTHREAD_REF:-master} rtthread_bsp=${RTTHREAD_BSP:-bsp/qemu-vexpress-a9} @@ -655,6 +792,7 @@ cc_cmd=${toolchain_prefix}gcc nm_cmd=${NM:-${toolchain_prefix}nm} size_cmd=${SIZE:-${toolchain_prefix}size} rtthread_elf=${RTTHREAD_ELF:-rtthread.elf} +rtthread_run_smoke=${RTTHREAD_RUN_SMOKE:-0} need_cmd "$cc_cmd" need_cmd "$nm_cmd" need_cmd "$size_cmd" @@ -674,7 +812,17 @@ prepare_rtthread_checkout "$rtthread_ref" "$rtthread_work" "$rtthread_dir" copy_package "$repo_root" "$package_dir" "$rtthread_work" write_packages_kconfig "$bsp_dir/packages" write_packages_sconscript "$bsp_dir/packages" -write_compile_check_source "$bsp_dir" +install_compile_check_source "$bsp_dir" "$ci_source_dir" +case "$rtthread_run_smoke" in + 1|yes|true|TRUE|on|ON) + install_smoke_sources "$bsp_dir" "$ci_source_dir" + ;; + 0|no|false|FALSE|off|OFF) + ;; + *) + fail "RTTHREAD_RUN_SMOKE must be 0 or 1: $rtthread_run_smoke" + ;; +esac export RTT_ROOT="$rtthread_dir" RTT_EXEC_PATH=${RTT_EXEC_PATH:-$(dirname "$(command -v "$cc_cmd")")} @@ -693,3 +841,9 @@ verify_rtconfig_symbols rtconfig.h "$rtthread_dfs_version" "$rtthread_dir" scons -j"$(getconf _NPROCESSORS_ONLN)" verify_symbols "$bsp_dir" "$nm_cmd" "$size_cmd" "$rtthread_elf" +case "$rtthread_run_smoke" in + 1|yes|true|TRUE|on|ON) + smoke_log_ref=$(safe_log_fragment "$rtthread_ref") + run_qemu_smoke "$bsp_dir" "$repo_root/_ci/qemu-${smoke_log_ref}-${rtthread_dfs_version}.log" + ;; +esac diff --git a/.github/ci/rtthread-littlefs/requirements.txt b/.github/ci/rtthread-littlefs/requirements.txt new file mode 100644 index 00000000..94b8dfac --- /dev/null +++ b/.github/ci/rtthread-littlefs/requirements.txt @@ -0,0 +1,2 @@ +kconfiglib==14.1.0 +scons==4.0.1 diff --git a/.github/ci/rtthread-littlefs/smoke/littlefs_compile_check.c b/.github/ci/rtthread-littlefs/smoke/littlefs_compile_check.c new file mode 100644 index 00000000..1a5f974c --- /dev/null +++ b/.github/ci/rtthread-littlefs/smoke/littlefs_compile_check.c @@ -0,0 +1,30 @@ +/* CI-only RT-Thread package integration check. */ +#include + +/* Match the package SConscript compile flag while including the real API. */ +/** + * @brief Select the package-local RT-Thread littlefs configuration header. + */ +#define LFS_CONFIG lfs_config.h +#include "../packages/littlefs/lfs.h" + +/** + * @brief Initialize the RT-Thread DFS littlefs package. + * + * @return 0 on success, otherwise a negative error code. + */ +extern int dfs_lfs_init(void); + +/** + * @brief Verify that package build graph linked littlefs symbols. + * + * @return 0 when required symbols are linked, otherwise -1. + */ +static int littlefs_compile_check(void) +{ + int (* volatile dfs_init)(void) = dfs_lfs_init; + int (* volatile mount)(lfs_t *, const struct lfs_config *) = lfs_mount; + + return (dfs_init != 0 && mount != 0) ? 0 : -1; +} +INIT_APP_EXPORT(littlefs_compile_check); diff --git a/.github/ci/rtthread-littlefs/smoke/littlefs_smoke.c b/.github/ci/rtthread-littlefs/smoke/littlefs_smoke.c new file mode 100644 index 00000000..ad45c463 --- /dev/null +++ b/.github/ci/rtthread-littlefs/smoke/littlefs_smoke.c @@ -0,0 +1,320 @@ +/* CI-only automatic littlefs smoke test for qemu-vexpress-a9. */ +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#if defined(DFS_USING_POSIX) +#include +#endif /* defined(DFS_USING_POSIX) */ + +#define LFS_SMOKE_DEV "lfsnor" +#define LFS_SMOKE_MNT "/lfs" +#define LFS_SMOKE_FILE "/lfs/hello.txt" +#define LFS_SMOKE_DIR "/lfs/dir1" +#define LFS_SMOKE_PAYLOAD "hello-littlefs-ci" +#define LFS_SMOKE_PAYLOAD_LEN (sizeof(LFS_SMOKE_PAYLOAD) - 1U) + +#if defined(RT_USING_DFS_V2) +/** @brief Absolute pseudo-source path accepted by DFS v2 pseudo filesystems. */ +#define LFS_SMOKE_PSEUDO_SOURCE "/" +#else +/** @brief Legacy pseudo-filesystem source argument. */ +#define LFS_SMOKE_PSEUDO_SOURCE RT_NULL +#endif /* defined(RT_USING_DFS_V2) */ + +/** @brief RT-Thread device object name used by dfs_mkfs() and dfs_mount(). */ +#define LFS_SMOKE_DFS_DEVICE LFS_SMOKE_DEV + +/** + * @brief ROMFS child entries used to provide stable smoke-test mount points. + */ +static const struct romfs_dirent littlefs_smoke_romfs_entries[] = +{ + { ROMFS_DIRENT_DIR, "dev", RT_NULL, 0 }, + { ROMFS_DIRENT_DIR, "lfs", RT_NULL, 0 }, +}; + +/** + * @brief ROMFS root directory used to provide stable smoke-test mount points. + */ +static const struct romfs_dirent littlefs_smoke_romfs_root = +{ + ROMFS_DIRENT_DIR, + "/", + (const rt_uint8_t *)littlefs_smoke_romfs_entries, + sizeof(littlefs_smoke_romfs_entries) / + sizeof(littlefs_smoke_romfs_entries[0]), +}; + +/** + * @brief Print a smoke-test failure marker and return the error code. + * + * @param step Name of the failed smoke-test step. + * @param err Error code returned by the failed step. + * @return The original error code, or -RT_ERROR when err is 0. + */ +static int littlefs_smoke_fail(const char *step, int err) +{ + rt_kprintf("LITTLEFS_SMOKE_FAIL step=%s err=%d\n", step, err); + return err != 0 ? err : -RT_ERROR; +} + +/** + * @brief Verify that the smoke-test file can be read and matches the payload. + * + * @return RT_EOK on success, otherwise an RT-Thread error code. + */ +static int littlefs_smoke_read_file(void) +{ + int fd; + int ret; + char buf[32]; + + rt_memset(buf, 0, sizeof(buf)); + fd = open(LFS_SMOKE_FILE, O_RDONLY, 0); + if (fd < 0) + { + return littlefs_smoke_fail("open-read", fd); + } + + ret = read(fd, buf, sizeof(buf) - 1U); + close(fd); + if (ret < 0) + { + return littlefs_smoke_fail("read", ret); + } + + if (strcmp(buf, LFS_SMOKE_PAYLOAD) != 0) + { + rt_kprintf("LITTLEFS_SMOKE_FAIL step=compare got=%s\n", buf); + return -RT_ERROR; + } + + return RT_EOK; +} + +/** + * @brief Verify directory traversal through the mounted littlefs filesystem. + * + * @return RT_EOK on success, otherwise an RT-Thread error code. + */ +static int littlefs_smoke_list_dir(void) +{ +#if defined(DFS_USING_POSIX) + DIR *dir; + struct dirent *entry; + int found_file = 0; + int found_dir = 0; + + dir = opendir(LFS_SMOKE_MNT); + if (dir == RT_NULL) + { + return littlefs_smoke_fail("opendir", -RT_ERROR); + } + + while ((entry = readdir(dir)) != RT_NULL) + { + if (rt_strcmp(entry->d_name, "hello.txt") == 0) + { + found_file = 1; + } + else if (rt_strcmp(entry->d_name, "dir1") == 0) + { + found_dir = 1; + } + } + closedir(dir); + + if (!found_file || !found_dir) + { + rt_kprintf("LITTLEFS_SMOKE_FAIL step=readdir file=%d dir=%d\n", + found_file, found_dir); + return -RT_ERROR; + } + + return RT_EOK; +#else + int fd; + int ret; + int found_file = 0; + int found_dir = 0; + struct dirent entries[4]; + + fd = open(LFS_SMOKE_MNT, O_RDONLY | O_DIRECTORY, 0); + if (fd < 0) + { + return littlefs_smoke_fail("open-dir", fd); + } + + do + { + int index; + + rt_memset(entries, 0, sizeof(entries)); + ret = getdents(fd, entries, sizeof(entries)); + if (ret < 0) + { + close(fd); + return littlefs_smoke_fail("getdents", ret); + } + + for (index = 0; index < ret / (int)sizeof(struct dirent); index++) + { + if (rt_strcmp(entries[index].d_name, "hello.txt") == 0) + { + found_file = 1; + } + else if (rt_strcmp(entries[index].d_name, "dir1") == 0) + { + found_dir = 1; + } + } + } while (ret > 0); + + close(fd); + if (!found_file || !found_dir) + { + rt_kprintf("LITTLEFS_SMOKE_FAIL step=getdents-scan file=%d dir=%d\n", + found_file, found_dir); + return -RT_ERROR; + } + + return RT_EOK; +#endif /* defined(DFS_USING_POSIX) */ +} + +/** + * @brief Prepare stable root, device, and littlefs mount-point filesystems. + * + * The qemu-vexpress-a9 BSP root filesystem depends on SD-card FAT behavior + * that differs across RT-Thread DFS versions. Mount a static ROMFS root with + * /dev and /lfs entries so the smoke test does not depend on ELMFAT, SD + * auto-mount, tmpfs, or mounting littlefs as root. DFS v2 mounts devfs at + * /dev during component initialization, so the smoke test does not remount it. + * + * @return RT_EOK on success, otherwise an RT-Thread error code. + */ +static int littlefs_smoke_prepare_rootfs(void) +{ + int ret; + + ret = dfs_mount(LFS_SMOKE_PSEUDO_SOURCE, + "/", + "rom", + 0, + (const void *)&littlefs_smoke_romfs_root); + rt_kprintf("LITTLEFS_SMOKE_INFO mount romfs root ret=%d\n", ret); + if (ret != 0) + { + return littlefs_smoke_fail("mount-root-romfs", ret); + } + +#if defined(RT_USING_DFS_V2) + rt_kprintf("LITTLEFS_SMOKE_INFO use existing devfs mount\n"); +#endif /* defined(RT_USING_DFS_V2) */ + + return RT_EOK; +} + +/** + * @brief Run the automatic littlefs filesystem smoke test. + * + * @return RT_EOK on success, otherwise an RT-Thread error code. + */ +static int littlefs_smoke_run(void) +{ + int fd; + int ret; + + rt_kprintf("LITTLEFS_SMOKE_START dev=%s\n", LFS_SMOKE_DEV); + + ret = littlefs_smoke_prepare_rootfs(); + if (ret != RT_EOK) + { + return ret; + } + + ret = dfs_mkfs("lfs", LFS_SMOKE_DFS_DEVICE); + if (ret != 0) + { + return littlefs_smoke_fail("mkfs", ret); + } + + ret = dfs_mount(LFS_SMOKE_DFS_DEVICE, LFS_SMOKE_MNT, "lfs", 0, RT_NULL); + if (ret != 0) + { + return littlefs_smoke_fail("mount", ret); + } + + fd = open(LFS_SMOKE_FILE, O_CREAT | O_RDWR | O_TRUNC, 0666); + if (fd < 0) + { + return littlefs_smoke_fail("open-write", fd); + } + + ret = write(fd, LFS_SMOKE_PAYLOAD, LFS_SMOKE_PAYLOAD_LEN); + close(fd); + if (ret != (int)LFS_SMOKE_PAYLOAD_LEN) + { + return littlefs_smoke_fail("write", ret); + } + + ret = mkdir(LFS_SMOKE_DIR, 0); + if (ret != 0) + { + return littlefs_smoke_fail("mkdir-dir", ret); + } + + ret = littlefs_smoke_read_file(); + if (ret != RT_EOK) + { + return ret; + } + + ret = littlefs_smoke_list_dir(); + if (ret != RT_EOK) + { + return ret; + } + + ret = dfs_unmount(LFS_SMOKE_MNT); + if (ret != 0) + { + return littlefs_smoke_fail("unmount", ret); + } + + ret = dfs_mount(LFS_SMOKE_DFS_DEVICE, LFS_SMOKE_MNT, "lfs", 0, RT_NULL); + if (ret != 0) + { + return littlefs_smoke_fail("remount", ret); + } + + ret = littlefs_smoke_read_file(); + if (ret != RT_EOK) + { + return ret; + } + + ret = littlefs_smoke_list_dir(); + if (ret != RT_EOK) + { + return ret; + } + + ret = dfs_unmount(LFS_SMOKE_MNT); + if (ret != 0) + { + return littlefs_smoke_fail("final-unmount", ret); + } + + rt_kprintf("LITTLEFS_SMOKE_PASS\n"); + return RT_EOK; +} +INIT_APP_EXPORT(littlefs_smoke_run); diff --git a/.github/ci/rtthread-littlefs/smoke/qemu_lfs_mtd.c b/.github/ci/rtthread-littlefs/smoke/qemu_lfs_mtd.c new file mode 100644 index 00000000..b470dd9f --- /dev/null +++ b/.github/ci/rtthread-littlefs/smoke/qemu_lfs_mtd.c @@ -0,0 +1,219 @@ +/* CI-only RAM-backed MTD NOR device for littlefs smoke tests. */ +#include +#include + +#define QEMU_LFS_MTD_BLOCK_SIZE 4096U +#define QEMU_LFS_MTD_BLOCK_COUNT 128U +#define QEMU_LFS_MTD_TOTAL_SIZE (QEMU_LFS_MTD_BLOCK_SIZE * QEMU_LFS_MTD_BLOCK_COUNT) + +/** @brief Number of RAM-backed MTD NOR devices used by the smoke test. */ +#define QEMU_LFS_MTD_DEVICE_COUNT 2U + +/** + * @brief One RAM-backed MTD NOR instance used by the QEMU smoke test. + */ +typedef struct +{ + struct rt_mtd_nor_device device; /**< RT-Thread MTD NOR device. */ + rt_uint8_t storage[QEMU_LFS_MTD_TOTAL_SIZE]; /**< Backing flash bytes. */ + const char *name; /**< Registered device name. */ +} qemu_lfs_mtd_instance_t; + +/** + * @brief RAM-backed MTD NOR devices used only for QEMU littlefs smoke tests. + */ +static qemu_lfs_mtd_instance_t qemu_lfs_mtd_devices[QEMU_LFS_MTD_DEVICE_COUNT] = +{ + { .name = "lfsroot" }, + { .name = "lfsnor" }, +}; + +/** + * @brief Find backing storage for an MTD NOR operation. + * + * @param device MTD NOR device passed by DFS/littlefs. + * @return Backing storage buffer, or RT_NULL for an unknown device. + */ +static rt_uint8_t *qemu_lfs_mtd_storage_of(struct rt_mtd_nor_device *device) +{ + rt_size_t index; + + for (index = 0U; index < QEMU_LFS_MTD_DEVICE_COUNT; index++) + { + if (&qemu_lfs_mtd_devices[index].device == device) + { + return qemu_lfs_mtd_devices[index].storage; + } + } + + return RT_NULL; +} + +/** + * @brief Accept an MTD NOR ID read request for the smoke-test device. + * + * The targeted RT-Thread releases declare the read_id driver op as returning + * rt_err_t, while the public rt_mtd_nor_read_id wrapper returns rt_uint32_t. + * The smoke test does not depend on a JEDEC ID value, so the RAM-backed device + * reports only that the operation completed successfully. + * + * @param device MTD NOR device. + * @return RT_EOK on success. + */ +static rt_err_t qemu_lfs_mtd_read_id(struct rt_mtd_nor_device *device) +{ + RT_UNUSED(device); + + return RT_EOK; +} + +/** + * @brief Return type used by RT-Thread MTD NOR read/write callbacks. + */ +#if RTTHREAD_VERSION >= RT_VERSION_CHECK(5, 0, 0) +typedef rt_ssize_t qemu_lfs_mtd_rw_ret_t; +#else +typedef rt_size_t qemu_lfs_mtd_rw_ret_t; +#endif /* RTTHREAD_VERSION >= RT_VERSION_CHECK(5, 0, 0) */ + +/** + * @brief Length type used by RT-Thread MTD NOR callbacks. + */ +#if RTTHREAD_VERSION >= RT_VERSION_CHECK(5, 0, 0) +typedef rt_size_t qemu_lfs_mtd_len_t; +#else +typedef rt_uint32_t qemu_lfs_mtd_len_t; +#endif /* RTTHREAD_VERSION >= RT_VERSION_CHECK(5, 0, 0) */ + +/** + * @brief Read bytes from the RAM-backed MTD NOR device. + * + * @param device MTD NOR device. + * @param offset Byte offset in the emulated NOR storage. + * @param data Destination buffer. + * @param length Number of bytes to read. + * @return Number of bytes read, or 0 on out-of-range access. + */ +static qemu_lfs_mtd_rw_ret_t qemu_lfs_mtd_read(struct rt_mtd_nor_device *device, + rt_off_t offset, + rt_uint8_t *data, + qemu_lfs_mtd_len_t length) +{ + rt_uint8_t *storage = qemu_lfs_mtd_storage_of(device); + + if ((storage == RT_NULL) || (data == RT_NULL) || (offset < 0) || + ((rt_size_t)offset >= QEMU_LFS_MTD_TOTAL_SIZE) || + (length > QEMU_LFS_MTD_TOTAL_SIZE - (rt_size_t)offset)) + { + return 0; + } + + rt_memcpy(data, &storage[offset], length); + return (qemu_lfs_mtd_rw_ret_t)length; +} + +/** + * @brief Write bytes to the RAM-backed MTD NOR device. + * + * @param device MTD NOR device. + * @param offset Byte offset in the emulated NOR storage. + * @param data Source buffer. + * @param length Number of bytes to write. + * @return Number of bytes written, or 0 on out-of-range access. + */ +static qemu_lfs_mtd_rw_ret_t qemu_lfs_mtd_write(struct rt_mtd_nor_device *device, + rt_off_t offset, + const rt_uint8_t *data, + qemu_lfs_mtd_len_t length) +{ + rt_uint8_t *storage = qemu_lfs_mtd_storage_of(device); + + if ((storage == RT_NULL) || (data == RT_NULL) || (offset < 0) || + ((rt_size_t)offset >= QEMU_LFS_MTD_TOTAL_SIZE) || + (length > QEMU_LFS_MTD_TOTAL_SIZE - (rt_size_t)offset)) + { + return 0; + } + + rt_memcpy(&storage[offset], data, length); + return (qemu_lfs_mtd_rw_ret_t)length; +} + +/** + * @brief Erase one or more blocks in the RAM-backed MTD NOR device. + * + * @param device MTD NOR device. + * @param offset Byte offset in the emulated NOR storage. + * @param length Number of bytes to erase. + * @return RT_EOK on success, otherwise an RT-Thread error code. + */ +static rt_err_t qemu_lfs_mtd_erase(struct rt_mtd_nor_device *device, + rt_off_t offset, + qemu_lfs_mtd_len_t length) +{ + rt_uint8_t *storage = qemu_lfs_mtd_storage_of(device); + + if ((storage == RT_NULL) || (offset < 0) || + ((rt_size_t)offset >= QEMU_LFS_MTD_TOTAL_SIZE) || + (length > QEMU_LFS_MTD_TOTAL_SIZE - (rt_size_t)offset)) + { + return -RT_EINVAL; + } + + if (((rt_size_t)offset % QEMU_LFS_MTD_BLOCK_SIZE) != 0U || + (length % QEMU_LFS_MTD_BLOCK_SIZE) != 0U) + { + return -RT_EINVAL; + } + + rt_memset(&storage[offset], 0xff, length); + return RT_EOK; +} + +/** + * @brief MTD NOR operation table for the QEMU littlefs smoke-test device. + */ +static const struct rt_mtd_nor_driver_ops qemu_lfs_mtd_ops = +{ + .read_id = qemu_lfs_mtd_read_id, + .read = qemu_lfs_mtd_read, + .write = qemu_lfs_mtd_write, + .erase_block = qemu_lfs_mtd_erase, +}; + +/** + * @brief Register a RAM-backed MTD NOR device for littlefs smoke tests. + * + * @return RT_EOK on success, otherwise an RT-Thread error code. + */ +static int qemu_lfs_mtd_init(void) +{ + rt_size_t index; + + for (index = 0U; index < QEMU_LFS_MTD_DEVICE_COUNT; index++) + { + rt_err_t ret; + + rt_memset(qemu_lfs_mtd_devices[index].storage, + 0xff, + sizeof(qemu_lfs_mtd_devices[index].storage)); + rt_memset(&qemu_lfs_mtd_devices[index].device, + 0, + sizeof(qemu_lfs_mtd_devices[index].device)); + + qemu_lfs_mtd_devices[index].device.block_start = 0; + qemu_lfs_mtd_devices[index].device.block_end = QEMU_LFS_MTD_BLOCK_COUNT; + qemu_lfs_mtd_devices[index].device.block_size = QEMU_LFS_MTD_BLOCK_SIZE; + qemu_lfs_mtd_devices[index].device.ops = &qemu_lfs_mtd_ops; + + ret = rt_mtd_nor_register_device(qemu_lfs_mtd_devices[index].name, + &qemu_lfs_mtd_devices[index].device); + if (ret != RT_EOK) + { + return ret; + } + } + + return RT_EOK; +} +INIT_DEVICE_EXPORT(qemu_lfs_mtd_init); diff --git a/.github/workflows/rtthread.yml b/.github/workflows/rtthread.yml index cab318b5..17b6f55c 100644 --- a/.github/workflows/rtthread.yml +++ b/.github/workflows/rtthread.yml @@ -17,93 +17,208 @@ defaults: shell: bash -euo pipefail {0} jobs: - compile-littlefs: - name: RT-Thread DFS compile + prepare-master-source: + name: Prepare RT-Thread master source runs-on: ubuntu-22.04 - timeout-minutes: 180 - - env: - RTTHREAD_ELF: rtthread.elf - RTT_CC: gcc - RTTHREAD_REUSE_WORKDIR: 1 - RTTHREAD_BUILD_PROFILES: | - v4.1.1 v1 bsp/qemu-vexpress-a9 - v5.0.0 v1 bsp/qemu-vexpress-a9 - v5.0.1 v1 bsp/qemu-vexpress-a9 - v5.0.1 v2 bsp/qemu-vexpress-a9 - v5.0.2 v1 bsp/qemu-vexpress-a9 - v5.0.2 v2 bsp/qemu-vexpress-a9 - master v1 bsp/qemu-vexpress-a9 - master v2 bsp/qemu-vexpress-a9 + timeout-minutes: 20 + + steps: + - name: Install source preparation dependencies + timeout-minutes: 10 + run: | + export DEBIAN_FRONTEND=noninteractive + + sudo apt-get update -qq \ + -o Acquire::Retries=5 \ + -o Acquire::http::Timeout=30 \ + -o Acquire::https::Timeout=30 + + timeout 8m sudo apt-get install -y --no-install-recommends \ + -o Acquire::Retries=5 \ + -o Acquire::http::Timeout=30 \ + -o Acquire::https::Timeout=30 \ + ca-certificates \ + git + + - name: Clone and pack RT-Thread master source + run: | + mkdir -p _ci/master-source-work _ci/artifacts + + for attempt in 1 2 3; do + printf 'cloning RT-Thread master source attempt %s/3\n' "$attempt" + if timeout 12m git clone --depth 1 --branch master \ + https://github.com/RT-Thread/rt-thread.git \ + _ci/master-source-work/rt-thread; then + break + fi + + rm -rf _ci/master-source-work/rt-thread + if [ "$attempt" = 3 ]; then + printf 'failed to clone RT-Thread master source\n' >&2 + exit 1 + fi + sleep 10 + done + + git -C _ci/master-source-work/rt-thread rev-parse HEAD \ + | tee _ci/artifacts/rt-thread-master.sha + tar -cf _ci/artifacts/rt-thread-master.tar \ + -C _ci/master-source-work rt-thread + test -s _ci/artifacts/rt-thread-master.tar + test -s _ci/artifacts/rt-thread-master.sha + + - name: Upload RT-Thread master source + uses: actions/upload-artifact@v4 + with: + name: rtthread-master-source + path: | + _ci/artifacts/rt-thread-master.tar + _ci/artifacts/rt-thread-master.sha + if-no-files-found: error + + test-littlefs-tag: + name: RT-Thread ${{ matrix.profile }} + runs-on: ubuntu-22.04 + timeout-minutes: 90 + + strategy: + fail-fast: false + max-parallel: 6 + matrix: + include: + - profile: v4.1.1-v1 + rtthread_ref: v4.1.1 + dfs_version: v1 + bsp: bsp/qemu-vexpress-a9 + - profile: v5.0.0-v1 + rtthread_ref: v5.0.0 + dfs_version: v1 + bsp: bsp/qemu-vexpress-a9 + - profile: v5.0.1-v1 + rtthread_ref: v5.0.1 + dfs_version: v1 + bsp: bsp/qemu-vexpress-a9 + - profile: v5.0.1-v2 + rtthread_ref: v5.0.1 + dfs_version: v2 + bsp: bsp/qemu-vexpress-a9 + - profile: v5.0.2-v1 + rtthread_ref: v5.0.2 + dfs_version: v1 + bsp: bsp/qemu-vexpress-a9 + - profile: v5.0.2-v2 + rtthread_ref: v5.0.2 + dfs_version: v2 + bsp: bsp/qemu-vexpress-a9 steps: - name: Checkout littlefs package uses: actions/checkout@v4 - - name: Install build dependencies + - name: Prepare RT-Thread source cache directory run: | - sudo apt-get update -qq - sudo apt-get install -y --no-install-recommends \ - ca-certificates \ - git \ - make \ - gcc-arm-none-eabi \ - libnewlib-arm-none-eabi \ - libstdc++-arm-none-eabi-newlib \ - scons \ - python3 \ - python3-pip - python3 -m pip install --user kconfiglib - echo "$HOME/.local/bin" >> "$GITHUB_PATH" - arm-none-eabi-gcc --version - scons --version - python3 --version - - - name: Compile littlefs against RT-Thread profiles + mkdir -p "${{ runner.temp }}/rtthread-src-cache/${{ matrix.rtthread_ref }}" + + - name: Cache RT-Thread source archive + id: cache-rtthread-source + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/rtthread-src-cache/${{ matrix.rtthread_ref }} + key: ${{ runner.os }}-rtthread-src-${{ matrix.rtthread_ref }}-v1 + restore-keys: | + ${{ runner.os }}-rtthread-src-${{ matrix.rtthread_ref }}- + + - name: Set up RT-Thread littlefs build + uses: ./.github/actions/setup-rtthread-build + with: + rtthread-ref: ${{ matrix.rtthread_ref }} + rtthread-dfs-version: ${{ matrix.dfs_version }} + rtthread-bsp: ${{ matrix.bsp }} + rtthread-source-cache-dir: ${{ runner.temp }}/rtthread-src-cache/${{ matrix.rtthread_ref }} + rtthread-source-cache-read: '1' + + - name: Compile and smoke-test littlefs profile run: | mkdir -p _ci/logs - printf '%s\n' "$RTTHREAD_BUILD_PROFILES" > _ci/rtthread-build-profiles.txt - - failed_profiles_file=_ci/failed-rtthread-profiles.txt - : > "$failed_profiles_file" - while read -r rtthread_ref dfs_version bsp; do - [ -n "${rtthread_ref:-}" ] || continue - case "$rtthread_ref" in - \#*) continue ;; - esac - - log_ref=${rtthread_ref//\//-} - log_file="_ci/logs/rtthread-${log_ref}-${dfs_version}.log" - printf 'building RT-Thread %s DFS %s with BSP %s\n' \ - "$rtthread_ref" "$dfs_version" "$bsp" - - if RTTHREAD_REF="$rtthread_ref" \ - RTTHREAD_DFS_VERSION="$dfs_version" \ - RTTHREAD_BSP="$bsp" \ - sh .github/ci/rtthread-littlefs/build.sh 2>&1 | tee "$log_file"; then - printf 'profile passed: %s %s %s\n' \ - "$rtthread_ref" "$dfs_version" "$bsp" - else - printf 'profile failed: %s %s %s\n' \ - "$rtthread_ref" "$dfs_version" "$bsp" >&2 - printf '%s %s %s\n' \ - "$rtthread_ref" "$dfs_version" "$bsp" >> "$failed_profiles_file" - fi - done < _ci/rtthread-build-profiles.txt + log_file="_ci/logs/rtthread-${{ matrix.profile }}.log" + + printf 'building RT-Thread %s DFS %s with BSP %s\n' \ + "$RTTHREAD_REF" "$RTTHREAD_DFS_VERSION" "$RTTHREAD_BSP" + + sh .github/ci/rtthread-littlefs/build.sh 2>&1 | tee "$log_file" + + - name: Upload RT-Thread compile logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: rtthread-test-logs-${{ matrix.profile }} + path: | + _ci/logs/*.log + _ci/qemu-*.log + if-no-files-found: warn + + test-littlefs-master: + name: RT-Thread ${{ matrix.profile }} + needs: prepare-master-source + runs-on: ubuntu-22.04 + timeout-minutes: 90 + + strategy: + fail-fast: false + max-parallel: 2 + matrix: + include: + - profile: master-v1 + rtthread_ref: master + dfs_version: v1 + bsp: bsp/qemu-vexpress-a9 + - profile: master-v2 + rtthread_ref: master + dfs_version: v2 + bsp: bsp/qemu-vexpress-a9 + + steps: + - name: Checkout littlefs package + uses: actions/checkout@v4 + + - name: Download RT-Thread master source artifact + uses: actions/download-artifact@v4 + with: + name: rtthread-master-source + path: ${{ runner.temp }}/rtthread-master-source + + - name: Verify RT-Thread master source artifact + run: | + test -s "${{ runner.temp }}/rtthread-master-source/rt-thread-master.tar" + test -s "${{ runner.temp }}/rtthread-master-source/rt-thread-master.sha" + printf 'using RT-Thread master source artifact: ' + cat "${{ runner.temp }}/rtthread-master-source/rt-thread-master.sha" + + - name: Set up RT-Thread littlefs build + uses: ./.github/actions/setup-rtthread-build + with: + rtthread-ref: ${{ matrix.rtthread_ref }} + rtthread-dfs-version: ${{ matrix.dfs_version }} + rtthread-bsp: ${{ matrix.bsp }} + rtthread-source-archive: ${{ runner.temp }}/rtthread-master-source/rt-thread-master.tar + rtthread-source-cache-read: '0' + + - name: Compile and smoke-test littlefs profile + run: | + mkdir -p _ci/logs + log_file="_ci/logs/rtthread-${{ matrix.profile }}.log" + + printf 'building RT-Thread %s DFS %s with BSP %s\n' \ + "$RTTHREAD_REF" "$RTTHREAD_DFS_VERSION" "$RTTHREAD_BSP" - if [ -s "$failed_profiles_file" ]; then - printf 'failed RT-Thread profiles:\n' >&2 - cat "$failed_profiles_file" >&2 - exit 1 - fi + sh .github/ci/rtthread-littlefs/build.sh 2>&1 | tee "$log_file" - name: Upload RT-Thread compile logs if: always() uses: actions/upload-artifact@v4 with: - name: rtthread-compile-logs + name: rtthread-test-logs-${{ matrix.profile }} path: | _ci/logs/*.log - _ci/rtthread-build-profiles.txt - _ci/failed-rtthread-profiles.txt + _ci/qemu-*.log if-no-files-found: warn