Преглед на файлове

feat: 添加archive-rootdir支持并优化构建和安装逻辑 (#114)

- 在TaskSource中添加archive-rootdir字段,支持指定压缩包的根目录
- 优化Executor的构建和安装逻辑,增加needs_build和needs_install方法
- 添加dadk配置文件时间戳支持,用于判断是否需要重新构建或安装
- 重构ArchiveSource和ArchiveFile,支持解压指定目录

Signed-off-by: longjin <longjin@DragonOS.org>
LoGin преди 1 седмица
родител
ревизия
0e223e3461

+ 5 - 0
dadk-config/src/common/task.rs

@@ -9,6 +9,11 @@ pub struct TaskSource {
     pub source: Source,
     #[serde(rename = "source-path")]
     pub source_path: String,
+    /// 把压缩包中的哪个目录作为根目录(可选)
+    ///
+    /// 仅当 source = "archive" 时生效
+    #[serde(rename = "archive-rootdir")]
+    pub archive_rootdir: Option<String>,
     /// 分支(可选,如果为空,则拉取master)branch和revision只能同时指定一个
     pub branch: Option<String>,
     /// 特定的提交的hash值(可选,如果为空,则拉取branch的最新提交)

+ 3 - 0
dadk-config/templates/config/userapp_config.toml

@@ -32,6 +32,9 @@ source = "git"
 # 路径或URL
 source-path = "https://git.mirrors.dragonos.org.cn/DragonOS-Community/test_git.git"
 
+# 把压缩包中的哪个目录作为根目录(可选),仅当 source = "archive" 时生效
+# archive-rootdir = ""
+
 # git标签或分支
 # 注意: branch和revision只能二选一,且source要设置为"git"
 revision = "01cdc56863"

+ 1 - 0
dadk-config/tests/test_user_config.rs

@@ -39,6 +39,7 @@ fn test_parse_dadk_user_config(ctx: &mut DadkConfigTestContext) {
             source_path: "https://git.mirrors.dragonos.org.cn/DragonOS-Community/test_git.git"
                 .to_string(),
             branch: None,
+            archive_rootdir: None,
             revision: Some("01cdc56863".to_string()),
         },
         depends: vec![

+ 96 - 41
dadk-user/src/executor/mod.rs

@@ -139,6 +139,11 @@ impl Executor {
             }
         }
 
+        // 设置dadk配置文件的时间戳
+        if let Some(ts) = self.entity.config_file_timestamp() {
+            task_log.set_dadk_config_timestamp(ts);
+        }
+
         self.task_data_dir
             .save_task_log(&task_log)
             .expect("Failed to save task log");
@@ -196,25 +201,52 @@ impl Executor {
         Ok(())
     }
 
-    fn build(&mut self) -> Result<(), ExecutorError> {
-        if let Some(status) = self.task_log().build_status() {
-            if let Some(build_time) = self.task_log().build_time() {
-                let mut last_modified = last_modified_time(&self.entity.file_path(), build_time)?;
-                last_modified = core::cmp::max(
-                    last_modified,
-                    last_modified_time(&self.src_work_dir(), build_time)?,
-                );
+    fn needs_build(&self) -> Result<bool, ExecutorError> {
+        // dadk配置文件更新了,需要重新构建
+        if self.config_file_updated() {
+            return Ok(true);
+        }
+        let task_log = self.task_log();
+        let build_status = task_log.build_status();
+        if build_status.is_none() {
+            return Ok(true);
+        }
 
-                if *status == BuildStatus::Success
-                    && (self.entity.task().build_once || last_modified < *build_time)
-                {
-                    info!(
-                        "Task {} has been built successfully, skip build.",
-                        self.entity.task().name_version()
-                    );
-                    return Ok(());
-                }
-            }
+        let build_time = task_log.build_time();
+        if build_time.is_none() {
+            return Ok(true);
+        }
+
+        let build_status = build_status.unwrap();
+        let build_time = build_time.unwrap();
+
+        let mut last_modified = last_modified_time(&self.entity.file_path(), build_time)?;
+        if let Some(ref d) = self.src_work_dir() {
+            last_modified = core::cmp::max(last_modified, last_modified_time(d, build_time)?);
+        }
+
+        // 跳过构建
+        if *build_status == BuildStatus::Success
+            && (self.entity.task().build_once || last_modified < *build_time)
+        {
+            return Ok(false);
+        }
+
+        return Ok(true);
+    }
+
+    fn config_file_updated(&self) -> bool {
+        let task_log = self.task_log();
+        task_log.dadk_config_timestamp() != self.entity.config_file_timestamp().as_ref()
+    }
+
+    fn build(&mut self) -> Result<(), ExecutorError> {
+        if !self.needs_build().unwrap_or(true) {
+            log::info!(
+                "No need to build: {}, skipping...",
+                self.entity.task().name_version()
+            );
+            return Ok(());
         }
 
         return self.do_build();
@@ -259,26 +291,49 @@ impl Executor {
         return Ok(());
     }
 
+    fn needs_install(&self) -> Result<bool, ExecutorError> {
+        // dadk配置文件更新了,需要重新安装
+        if self.config_file_updated() {
+            return Ok(true);
+        }
+        let task_log = self.task_log();
+        let install_status = task_log.install_status();
+        if install_status.is_none() {
+            return Ok(true);
+        }
+
+        let install_time = task_log.install_time();
+        if install_time.is_none() {
+            return Ok(true);
+        }
+
+        let install_status = install_status.unwrap();
+        let install_time = install_time.unwrap();
+
+        let last_modified = last_modified_time(&self.build_dir.path, install_time)?;
+        let last_modified = core::cmp::max(
+            last_modified,
+            last_modified_time(&self.entity.file_path(), install_time)?,
+        );
+
+        // 跳过构建
+        if *install_status == InstallStatus::Success
+            && (self.entity.task().install_once || last_modified < *install_time)
+        {
+            return Ok(false);
+        }
+
+        return Ok(true);
+    }
+
     fn install(&self) -> Result<(), ExecutorError> {
         log::trace!("dadk-user: install {}", self.entity.task().name_version());
-        if let Some(status) = self.task_log().install_status() {
-            if let Some(install_time) = self.task_log().install_time() {
-                let last_modified = last_modified_time(&self.build_dir.path, install_time)?;
-                let last_modified = core::cmp::max(
-                    last_modified,
-                    last_modified_time(&self.entity.file_path(), install_time)?,
-                );
-
-                if *status == InstallStatus::Success
-                    && (self.entity.task().install_once || last_modified < *install_time)
-                {
-                    info!(
-                        "install: Task {} not changed.",
-                        self.entity.task().name_version()
-                    );
-                    return Ok(());
-                }
-            }
+        if !self.needs_install().unwrap_or(false) {
+            info!(
+                "install: Task {} not changed.",
+                self.entity.task().name_version()
+            );
+            return Ok(());
         }
         log::trace!(
             "dadk-user: to do install {}",
@@ -405,17 +460,17 @@ impl Executor {
         info!(
             "{}: Cleaning cache directory: {}",
             self.entity.task().name_version(),
-            self.src_work_dir().display()
+            cache_dir.as_ref().unwrap().path.display()
         );
         return cache_dir.unwrap().remove_self_recursive();
     }
 
     /// 获取源文件的工作目录
-    fn src_work_dir(&self) -> PathBuf {
+    fn src_work_dir(&self) -> Option<PathBuf> {
         if let Some(local_path) = self.entity.task().source_path() {
-            return local_path;
+            return Some(local_path);
         }
-        return self.source_dir.as_ref().unwrap().path.clone();
+        return Some(self.source_dir.as_ref()?.path.clone());
     }
 
     fn task_log(&self) -> TaskLog {
@@ -452,7 +507,7 @@ impl Executor {
         let raw_cmd = raw_cmd.unwrap();
 
         let mut command = Command::new("bash");
-        command.current_dir(self.src_work_dir());
+        command.current_dir(self.src_work_dir().unwrap());
 
         // 设置参数
         command.arg("-c");

+ 122 - 63
dadk-user/src/executor/source.rs

@@ -495,13 +495,18 @@ impl LocalSource {
 pub struct ArchiveSource {
     /// 压缩包的URL
     url: String,
+    /// 把压缩包中的哪个目录作为根目录(可选)
+    /// 默认是压缩包内的根目录
+    #[serde(default)]
+    rootdir: Option<String>,
 }
 
 impl ArchiveSource {
     #[allow(dead_code)]
-    pub fn new(url: String) -> Self {
-        Self { url }
+    pub fn new(url: String, rootdir: Option<String>) -> Self {
+        Self { url, rootdir }
     }
+
     pub fn validate(&self) -> Result<()> {
         if self.url.is_empty() {
             return Err(Error::msg("url is empty"));
@@ -518,6 +523,13 @@ impl ArchiveSource {
         } else {
             return Err(Error::msg(format!("url {:?} is not a valid url", self.url)));
         }
+
+        if self.rootdir.is_some() && self.rootdir.as_ref().unwrap().starts_with('/') {
+            return Err(Error::msg(format!(
+                "archive rootdir {:?} starts with '/'",
+                self.rootdir
+            )));
+        }
         return Ok(());
     }
 
@@ -557,20 +569,22 @@ impl ArchiveSource {
         }
         //创建临时目录
         std::fs::create_dir(path).map_err(|e| e.to_string())?;
-        info!("downloading {:?}", archive_name);
+        info!("downloading {:?}, url: {:?}", archive_name, self.url);
         FileUtils::download_file(&self.url, path).map_err(|e| e.to_string())?;
         //下载成功,开始尝试解压
         info!("download {:?} finished, start unzip", archive_name);
         let archive_file = ArchiveFile::new(&path.join(archive_name));
-        archive_file.unzip()?;
+        archive_file.unzip(self.rootdir.as_ref())?;
         //删除创建的临时文件夹
-        std::fs::remove_dir_all(path).map_err(|e| e.to_string())?;
+        // std::fs::remove_dir_all(path).map_err(|e| e.to_string())?;
         return Ok(());
     }
 }
 
 pub struct ArchiveFile {
+    /// archive file所在目录
     archive_path: PathBuf,
+    /// 压缩文件名
     archive_name: String,
     archive_type: ArchiveType,
 }
@@ -600,6 +614,89 @@ impl ArchiveFile {
         }
     }
 
+    fn do_unzip_tar_file(&self, in_archive_rootdir: Option<&String>) -> Result<(), String> {
+        let mut cmd = Command::new("tar");
+        cmd.arg("-xf").arg(&self.archive_name);
+
+        // 处理in_archive_rootdir参数,只解压压缩文件内的指定目录
+        if let Some(in_archive_rootdir) = in_archive_rootdir {
+            let mut components = 0;
+            in_archive_rootdir.split('/').for_each(|x| {
+                if x != "" {
+                    components += 1;
+                }
+            });
+
+            cmd.arg(format!("--strip-components={}", components));
+            cmd.arg(&in_archive_rootdir);
+        }
+
+        cmd.current_dir(&self.archive_path);
+
+        log::debug!("unzip tar file: {:?}", cmd);
+
+        let proc: std::process::Child = cmd
+            .stderr(Stdio::piped())
+            .stdout(Stdio::inherit())
+            .spawn()
+            .map_err(|e| e.to_string())?;
+        let output = proc.wait_with_output().map_err(|e| e.to_string())?;
+        if !output.status.success() {
+            return Err(format!(
+                "unzip tar file failed, status: {:?},  stderr: {:?}",
+                output.status,
+                StdioUtils::tail_n_str(StdioUtils::stderr_to_lines(&output.stderr), 5)
+            ));
+        }
+        Ok(())
+    }
+
+    fn do_unzip_zip_file(&self, in_archive_rootdir: Option<&String>) -> Result<(), String> {
+        let file =
+            File::open(&self.archive_path.join(&self.archive_name)).map_err(|e| e.to_string())?;
+        let mut archive = ZipArchive::new(file).map_err(|e| e.to_string())?;
+        for i in 0..archive.len() {
+            let mut file = archive.by_index(i).map_err(|e| e.to_string())?;
+            let file_name = file.name();
+
+            // 处理in_archive_rootdir参数,只解压指定目录下的内容
+            let outpath = if let Some(rootdir) = in_archive_rootdir {
+                if !file_name.starts_with(rootdir) {
+                    continue;
+                }
+                // 去除rootdir前缀,保留剩余路径
+                let relative_path = file_name.strip_prefix(rootdir).unwrap();
+                let relative_path = relative_path.trim_start_matches("/");
+                self.archive_path.join(relative_path)
+            } else {
+                match file.enclosed_name() {
+                    Some(path) => self.archive_path.join(path),
+                    None => continue,
+                }
+            };
+            if (*file.name()).ends_with('/') {
+                std::fs::create_dir_all(&outpath).map_err(|e| e.to_string())?;
+            } else {
+                if let Some(p) = outpath.parent() {
+                    if !p.exists() {
+                        std::fs::create_dir_all(&p).map_err(|e| e.to_string())?;
+                    }
+                }
+                let mut outfile = File::create(&outpath).map_err(|e| e.to_string())?;
+                std::io::copy(&mut file, &mut outfile).map_err(|e| e.to_string())?;
+            }
+            //设置解压后权限,在Linux中Unzip会丢失权限
+            #[cfg(unix)]
+            {
+                if let Some(mode) = file.unix_mode() {
+                    std::fs::set_permissions(&outpath, std::fs::Permissions::from_mode(mode))
+                        .map_err(|e| e.to_string())?;
+                }
+            }
+        }
+        Ok(())
+    }
+
     /// @brief 对self.archive_path路径下名为self.archive_name的压缩文件(tar.gz或zip)进行解压缩
     ///
     /// 在此函数中进行路径和文件名有效性的判断,如果有效的话就开始解压缩,根据ArchiveType枚举类型来
@@ -608,8 +705,7 @@ impl ArchiveFile {
     ///
     ///
     /// @return 根据结果返回OK或Err
-
-    pub fn unzip(&self) -> Result<(), String> {
+    pub fn unzip(&self, in_archive_rootdir: Option<&String>) -> Result<(), String> {
         let path = &self.archive_path;
         if !path.is_dir() {
             return Err(format!("Archive directory {:?} is wrong", path));
@@ -623,57 +719,11 @@ impl ArchiveFile {
         //根据压缩文件的类型生成cmd指令
         match &self.archive_type {
             ArchiveType::TarGz | ArchiveType::TarXz => {
-                let mut cmd = Command::new("tar");
-                cmd.arg("-xf").arg(&self.archive_name);
-                let proc: std::process::Child = cmd
-                    .current_dir(path)
-                    .stderr(Stdio::piped())
-                    .stdout(Stdio::inherit())
-                    .spawn()
-                    .map_err(|e| e.to_string())?;
-                let output = proc.wait_with_output().map_err(|e| e.to_string())?;
-                if !output.status.success() {
-                    return Err(format!(
-                        "unzip failed, status: {:?},  stderr: {:?}",
-                        output.status,
-                        StdioUtils::tail_n_str(StdioUtils::stderr_to_lines(&output.stderr), 5)
-                    ));
-                }
+                self.do_unzip_tar_file(in_archive_rootdir)?;
             }
 
             ArchiveType::Zip => {
-                let file = File::open(&self.archive_path.join(&self.archive_name))
-                    .map_err(|e| e.to_string())?;
-                let mut archive = ZipArchive::new(file).map_err(|e| e.to_string())?;
-                for i in 0..archive.len() {
-                    let mut file = archive.by_index(i).map_err(|e| e.to_string())?;
-                    let outpath = match file.enclosed_name() {
-                        Some(path) => self.archive_path.join(path),
-                        None => continue,
-                    };
-                    if (*file.name()).ends_with('/') {
-                        std::fs::create_dir_all(&outpath).map_err(|e| e.to_string())?;
-                    } else {
-                        if let Some(p) = outpath.parent() {
-                            if !p.exists() {
-                                std::fs::create_dir_all(&p).map_err(|e| e.to_string())?;
-                            }
-                        }
-                        let mut outfile = File::create(&outpath).map_err(|e| e.to_string())?;
-                        std::io::copy(&mut file, &mut outfile).map_err(|e| e.to_string())?;
-                    }
-                    //设置解压后权限,在Linux中Unzip会丢失权限
-                    #[cfg(unix)]
-                    {
-                        if let Some(mode) = file.unix_mode() {
-                            std::fs::set_permissions(
-                                &outpath,
-                                std::fs::Permissions::from_mode(mode),
-                            )
-                            .map_err(|e| e.to_string())?;
-                        }
-                    }
-                }
+                self.do_unzip_zip_file(in_archive_rootdir)?;
             }
             _ => {
                 return Err("unsupported archive type".to_string());
@@ -683,14 +733,23 @@ impl ArchiveFile {
         info!("unzip successfully, removing archive ");
         std::fs::remove_file(path.join(&self.archive_name)).map_err(|e| e.to_string())?;
         //从解压的文件夹中提取出文件并删除下载的压缩包等价于指令"cd *;mv ./* ../../"
-        for entry in path.read_dir().map_err(|e| e.to_string())? {
-            let entry = entry.map_err(|e| e.to_string())?;
-            let path = entry.path();
-            FileUtils::move_files(&path, &self.archive_path.parent().unwrap())
-                .map_err(|e| e.to_string())?;
-            //删除空的单独文件夹
-            std::fs::remove_dir_all(&path).map_err(|e| e.to_string())?;
-        }
+        // for entry in path.read_dir().map_err(|e| e.to_string())? {
+        //     let entry = entry.map_err(|e| e.to_string())?;
+        //     let path = entry.path();
+        //     FileUtils::move_files(&path, &self.archive_path.parent().unwrap())
+        //         .map_err(|e| e.to_string())?;
+        //     //删除空的单独文件夹
+        //     std::fs::remove_dir_all(&path).map_err(|e| e.to_string())?;
+        // }
+        std::process::Command::new("sh")
+            .arg("-c")
+            .arg(format!(
+                "mv {p}/* {parent} && rm -rf {p}",
+                p = &self.archive_path.to_string_lossy(),
+                parent = self.archive_path.parent().unwrap().to_string_lossy()
+            ))
+            .output()
+            .map_err(|e| e.to_string())?;
         return Ok(());
     }
 }

+ 2 - 2
dadk-user/src/parser/task.rs

@@ -294,7 +294,7 @@ impl TryFrom<TaskSource> for TaskType {
                     LocalSource::new(PathBuf::from(task_source.source_path)),
                 ))),
                 Source::Archive => Ok(TaskType::BuildFromSource(CodeSource::Archive(
-                    ArchiveSource::new(task_source.source_path),
+                    ArchiveSource::new(task_source.source_path, task_source.archive_rootdir),
                 ))),
             },
             TaskSourceType::InstallFromPrebuilt => match task_source.source {
@@ -305,7 +305,7 @@ impl TryFrom<TaskSource> for TaskType {
                     LocalSource::new(PathBuf::from(task_source.source_path)),
                 ))),
                 Source::Archive => Ok(TaskType::InstallFromPrebuilt(PrebuiltSource::Archive(
-                    ArchiveSource::new(task_source.source_path),
+                    ArchiveSource::new(task_source.source_path, task_source.archive_rootdir),
                 ))),
             },
         }

+ 11 - 0
dadk-user/src/parser/task_log.rs

@@ -20,6 +20,8 @@ pub struct TaskLog {
     build_status: Option<BuildStatus>,
     /// 任务安装状态
     install_status: Option<InstallStatus>,
+    /// dadk配置文件的时间戳
+    dadk_config_timestamp: Option<DateTime<Utc>>,
 }
 
 fn ok_or_default<'a, T, D>(deserializer: D) -> Result<T, D::Error>
@@ -39,9 +41,18 @@ impl TaskLog {
             build_status: None,
             install_timestamp: None,
             install_status: None,
+            dadk_config_timestamp: None,
         }
     }
 
+    pub fn dadk_config_timestamp(&self) -> Option<&DateTime<Utc>> {
+        self.dadk_config_timestamp.as_ref()
+    }
+
+    pub fn set_dadk_config_timestamp(&mut self, time: DateTime<Utc>) {
+        self.dadk_config_timestamp = Some(time);
+    }
+
     #[allow(dead_code)]
     pub fn set_build_time(&mut self, time: DateTime<Utc>) {
         self.build_timestamp = Some(time);

+ 6 - 0
dadk-user/src/scheduler/mod.rs

@@ -10,6 +10,7 @@ use std::{
     thread::ThreadId,
 };
 
+use chrono::{DateTime, Utc};
 use log::{error, info};
 
 use crate::{
@@ -71,6 +72,11 @@ impl SchedEntity {
         self.inner.lock().unwrap().task.clone()
     }
 
+    pub fn config_file_timestamp(&self) -> Option<DateTime<Utc>> {
+        let meta = self.file_path().metadata().ok()?;
+        meta.modified().map(|t| DateTime::<Utc>::from(t)).ok()
+    }
+
     /// 入度加1
     pub fn add_indegree(&self) {
         self.inner.lock().unwrap().indegree += 1;

+ 0 - 16
dadk-user/src/utils/file.rs

@@ -28,22 +28,6 @@ impl FileUtils {
         Ok(())
     }
 
-    /// 把指定路径下所有文件和文件夹递归地移动到另一个文件中
-    pub fn move_files(src: &Path, dst: &Path) -> std::io::Result<()> {
-        for entry in src.read_dir()? {
-            let entry = entry?;
-            let path = entry.path();
-            let new_path = dst.join(path.file_name().unwrap());
-            if entry.file_type()?.is_dir() {
-                std::fs::create_dir_all(&new_path)?;
-                FileUtils::move_files(&path, &new_path)?;
-            } else {
-                std::fs::rename(&path, &new_path)?;
-            }
-        }
-        Ok(())
-    }
-
     /// 递归地复制给定目录下所有文件到另一个文件夹中
     pub fn copy_dir_all(src: &Path, dst: &Path) -> Result<(), String> {
         log::trace!("FileUtils::copy_dir_all: src: {:?}, dst: {:?}", src, dst);

+ 3 - 0
docs/user-manual/quickstart.md

@@ -138,6 +138,9 @@ source = "local"
 # 路径或URL
 source-path = "user/apps/helloworld"
 
+# 把压缩包中的哪个目录作为根目录(可选),仅当 source = "archive" 时生效
+# archive-rootdir = ""
+
 # 构建相关信息
 [build]
 # (可选)构建命令