refactor: 迁移 dotfiles 管理部分为独立仓库

This commit is contained in:
KAAAsS 2023-06-15 23:32:07 +08:00
parent 6b7455d9eb
commit a4663a3c2e
Signed by: KAAAsS
GPG Key ID: D22F53AF662411FE
23 changed files with 1361 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
work
.DS_Store

22
active_config.nu Normal file
View File

@ -0,0 +1,22 @@
# active_config.nu -- 当前活动的 dotfiles 配置
#
# dotfiles 可用的配置为一个 Nushell 模块,通过文件夹形式组织:`<模块名>/mod.nu`。
# 模块需要声明 `register` 函数以通过 dotfiles 模块进行当前模块的信息注册。如:
#
# > use ../knotfiles/dotfile *
# >
# > def-env register [] {
# > declare "example" "example/example.yml" ".config/example.yml"
# > }
#
# 若要启用一个配置,则需要在此文件中导入该模块,并在 `register` 函数中调用对应模块的
# `register` 函数。
#
# Copyright (C) 2022-2023 KAAAsS
use example_module
# 注册全部模块
export def-env register [] {
example_module register
}

131
dotfiles Executable file
View File

@ -0,0 +1,131 @@
#!/usr/bin/env nu
# dotfiles -- 操作 dotfiles 相关环境的 CLI 工具
#
# 使用方式dotfiles <action>
#
# Copyright (C) 2022 KAAAsS
use knotfiles/log.nu
use knotfiles/dotfile
use active_config.nu
# 安装相关环境
def install [
modules
] {
log status $"安装路径:($env.HOME_DIR)\n"
# 安装依赖包
log succ "开始安装依赖包"; log info ""
dotfile do_hook $modules "install_dep" "依赖包安装"
# 运行安装前钩子
log succ "开始运行安装前钩子"; log info ""
dotfile do_hook $modules "pre_install" "安装前钩子"
# 链接
log succ "开始进行链接"; log info ""
dotfile do_link $modules
# 运行安装后钩子
log succ "开始运行安装后钩子"; log info ""
dotfile do_hook $modules "post_install" "安装后钩子"
log succ "安装完成!"
}
# 卸载相关环境
def uninstall [
modules
] {
log status $"安装路径:($env.HOME_DIR)\n"
# 运行卸载前钩子
log succ "开始运行卸载前钩子"; log info ""
dotfile do_hook $modules "pre_uninstall" "卸载前钩子"
# 清理
log succ "开始清理文件"; log info ""
dotfile do_unlink $modules
# 运行卸载后钩子
log succ "开始运行卸载后钩子"; log info ""
dotfile do_hook $modules "post_uninstall" "卸载后钩子"
# 卸载依赖包
log succ "开始卸载依赖包"; log info ""
dotfile do_hook $modules "uninstall_dep" "依赖包卸载"
log succ "卸载完成!"
}
# 同步配置文件
def sync [
modules
] {
# 链接
log succ "开始进行链接"; log info ""
dotfile do_link $modules
}
# 列举管理的配置文件
def list [
modules,
list_installed,
list_module,
] {
if ($list_module) {
dotfile filtered_modules $modules
} else {
let files = (
dotfile list_files $modules | select module dest | rename module path
)
if ($list_installed) {
$files | where ($it.path | path type) == "symlink"
} else {
$files
}
}
}
# 列举并检查所有环境约束
def constraints [] {
dotfile check_all_constraints
}
# knotfiles -- 操作 dotfiles 相关环境的 CLI 工具
def main [
action: string, # 待进行的操作。支持: install, uninstall, sync, list (ls), constraints
...modules: string, # 欲操作的模块,为空则处理全部模块
--home-dir (-H): string, # 家目录(即安装目录)
--verbose (-v), # 打印调试信息
--less, # 减少输出的内容
--list-installed (-I), # (list) 仅显示已安装的文件
--list-module (-M), # (list) 仅列举模块
--no-confirm, # 所有询问选择默认选项
--ignore-constraint (-C), # 忽略模块的约束检查
] {
# 家目录
let-env HOME_DIR = (
if ($home_dir != null) { $home_dir } else { $env.HOME }
| path expand
)
# 日志等级
let-env LOG_LEVEL = (
if ($verbose) { 0 }
else { if ($less) { 12 } else { 10 } }
)
# 确认选项
let-env NO_CONFIRM = $no_confirm
# 约束检查
let-env IGNORE_CONSTRAINT = $ignore_constraint
# 加载配置集
active_config register
# 运行操作
if ($action == install) {
install $modules
} else if ($action == sync) {
sync $modules
} else if ($action == uninstall) {
uninstall $modules
} else if ($action == list or $action == ls) {
list $modules $list_installed $list_module
} else if ($action == constraints) {
constraints
} else {
log error "操作不存在!"
exit 1
}
}

1
example_module/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
example.local.yml

3
example_module/README.md Normal file
View File

@ -0,0 +1,3 @@
# 示例配置模块
本目录所示的 Nushell 包为示例的一个 dotfile 模块。

View File

@ -0,0 +1 @@
# 本地配置文件的模板。实际的配置文件 example.local.yml 不会进行版本管理。

View File

@ -0,0 +1 @@
# 仅供示例,无实际作用

90
example_module/mod.nu Normal file
View File

@ -0,0 +1,90 @@
# mod.nu -- example dotfile 的配置
#
# Copyright (C) 2022 KAAAsS
use ../knotfiles/log.nu
use ../knotfiles/dotfile *
export def module_name [] {
"example_module"
}
# 声明模块相关的文件
export def-env declare_files [] {
# 普通文件的声明。目标文件位于 ~/.config/example.yml
declare (module_name) "example_module/example.yml" ".config/example.yml"
# 增加 --local-only 则可以声明本地文件。此类文件用于管理本地环境相关的配置,不参与同步。
# 如果相关的本地文件不存在,则会根据对应的 .example 文件创建一个。
declare (module_name) "example_module/example.local.yml" ".config/example.local.yml" --local-only
}
# 声明环境约束
export def constraints [] {
# 环境约束用于根据当前安装的环境来确定该配置模块是否应该被启用。如:当约束包含了 "os/linux"
# 则只有在当前系统为 linux 时才会启用该模块,否则该配置会在实际安装时被忽略。可以使用命令
# `./dotfiles constraints` 来查看当前支持的全部约束,及其在当前环境中是否被满足。
#
# 此外,用户配置中也可以定义新的约束。建议在模块的 `register` 函数的一开始进行定义。
#
# ```nushell
# use ../knotfiles/dotfile
#
# dotfile register_custom_constraint "kde5" {
# use ../utils/pkg.nu
#
# pkg check_install? "plasma-desktop"
# }
# ```
#
[ "os/linux", "gui" ]
}
# 声明依赖包
export def dependencies [] {
# 可以设置包管理器,这样只有对应包管理器的包才会被安装,若无匹配则忽略
[
# 依赖包会在安装前钩子运行前(同理也在安装配置前)进行检查,如果依赖包未安装,则会自动安装
"bat",
# 可以通过 record 设置包名。只有当前环境中存在对应的包管理器时才会安装对应的包,否则会直接
# 忽略安装。例如,下列配置在 Ubuntu包管理器为 apt下会被直接忽略。
{
"pacman": "bind-tools",
"homebrew": "bind",
}
]
}
# 安装前钩子,用于配置环境等等
export def pre_install [] {
# 如果需要使用家目录,则应该使用:$env.HOME_DIR
log info "将在安装前触发"
}
# 安装后钩子,用于配置启动项等等
export def post_install [] {
log info "将在安装后触发"
}
# 卸载前钩子,用于停止程序等等
export def pre_uninstall [] {
# 如果需要使用家目录,则应该使用:$env.HOME_DIR
log info "将在卸载前触发"
}
# 卸载后钩子,用于还原环境等等
export def post_uninstall [] {
log info "将在卸载后触发"
}
# 注册当前模块
export def-env register [] {
declare_files
require_constraints (module_name) (constraints)
require_packages (module_name) (dependencies)
register_pre_install (module_name) { pre_install }
register_post_install (module_name) { post_install }
register_pre_uninstall (module_name) { pre_uninstall }
register_post_uninstall (module_name) { post_uninstall }
}

29
knotfiles/autostart.nu Normal file
View File

@ -0,0 +1,29 @@
# autostart.nu -- 当前用户开机自启配置
#
# Copyright (C) 2022 KAAAsS
# 设置自启脚本
export def install [
path: path
] {
let filename = ($path | path basename)
mkdir $"($env.HOME_DIR)/.config/autostart"
echo $"[Desktop Entry]
Exec=($path)
Icon=dialog-scripts
Name=($filename)
Path=
Type=Application
X-KDE-AutostartScript=true
" | save -f $"($env.HOME_DIR)/.config/autostart/($filename).desktop"
null
}
# 取消自启脚本
export def uninstall [
path: path
] {
let filename = ($path | path basename)
rm -f $"($env.HOME_DIR)/.config/autostart/($filename).desktop"
null
}

43
knotfiles/constraints.nu Normal file
View File

@ -0,0 +1,43 @@
# constraints.nu -- 内建约束定义
#
# Copyright (C) 2022 KAAAsS
# 操作系统相关约束
module os {
export def linux [] {
if (which uname | is-empty) {
false
} else {
(uname -s) == "Linux"
}
}
export def darwin [] {
if (which uname | is-empty) {
false
} else {
(uname -s) == "Darwin"
}
}
}
# 图形界面约束
export def gui [] {
use pkg.nu
pkg check_install? libxrandr
}
export use os
# 获得默认的约束
export def default_constraints [] {
{
"gui": {|| gui },
"false": {|| false },
# 操作系统
"os/linux": {|| os linux },
"os/darwin": {|| os darwin },
}
}

View File

@ -0,0 +1,73 @@
# constraint.nu -- 模块约束管理
#
# Copyright (C) 2022-2023 KAAAsS
use ../log.nu
use global_conf.nu
# 注册自定义约束
export def-env register_custom_constraint [
name: string, # 约束名
fn: block, # 约束检查函数,返回 bool
] {
log debug $"自定义约束: ($name): ($fn)"
let constraints = (global_conf get_config "_" "constraints")
global_conf set_config "_" "constraints" (
$constraints | merge { $name: $fn }
)
}
# 设置模块约束
export def-env require_constraints [
module: string,
constraints: list,
] {
log debug $"声明约束: ($module): ($constraints)"
# 检查约束存在
$constraints | each { |$it| check_constraint --no-run $it }
# 设置约束
global_conf set_config $module "constraints" $constraints
}
# 检查某个约束
export def check_constraint [
name: string,
--no-run
] {
let span = (metadata $name).span;
let constraints = (global_conf get_config "_" "constraints")
# 检查约束是否存在
if (not ($name in $constraints)) {
error make {
msg: $"约束 ($name) 不存在",
label: {
text: "约束名",
start: $span.start,
end: $span.end
}
}
}
# 运行
if (not $no_run) {
do ($constraints | get $name)
}
}
# 检查所有存在的约束
export def check_all_constraints [] {
let constraints = (global_conf get_config "_" "constraints")
mut result = null
for $cons in ($constraints | transpose key).key {
$result = ($result | append {
"constraint": $cons,
"result": (check_constraint $cons),
})
}
return $result
}

View File

@ -0,0 +1,88 @@
# global_conf.nu -- 管理全局 dotfile 配置
#
# Copyright (C) 2022-2023 KAAAsS
use ../constraints.nu
export-env {
# 全局配置
let-env modules_config = { "_":
{ "constraints": (constraints default_constraints) }
}
}
# 获取模块的专有配置
export def get_config [
module: string,
config: string,
default?
] {
let modules_config = $env.modules_config
# 获取模块配置
let modules = ($modules_config | transpose key).key
let module_config = if ($module in $modules) {
$modules_config | get $module
} else {
{"_": null}
}
# 获取配置项
let configs = ($module_config | transpose key).key
if ($config in $configs) {
$module_config | get $config
} else {
$default
}
}
# 设置模块的专有配置
export def-env set_config [
module: string, # 模块名
config: string, # 配置项
value # 值
] {
let modules_config = $env.modules_config
# 检查模块配置是否存在
let modules = ($modules_config | transpose key).key
let old_config = if ($module in $modules) {
$modules_config | get $module
} else {
{}
}
# 设置新配置
let config = (
$old_config
| merge { $config : $value }
)
let-env modules_config = (
$modules_config
| merge { $module : $config }
)
}
# 获得模块列表
export def modules [] {
$env.modules_config
| transpose name config | where name != "_"
}
# 运行
export def test [] {
# TODO: assert
set_config "test" "foo" "2333"
print "test.foo:" (get_config "test" "foo")
set_config "test" "bar" [1, 2, 3]
print "test.bar:" (get_config "test" "bar")
print "test.foobar:" (get_config "test" "foobar" "default")
print "foobar.foobar:" (get_config "foobar" "foobar" "default")
set_config "test2" "bar" [1, 2, 3]
print "modules:" (modules)
# 结果
print "modules_config:" $env.modules_config
print "test:" $env.modules_config.test
}

95
knotfiles/dotfile/hook.nu Normal file
View File

@ -0,0 +1,95 @@
# hook.nu -- 钩子管理
#
# Copyright (C) 2022-2023 KAAAsS
use ../log.nu
use ../pkg.nu
use ../interactive.nu [confirm]
use global_conf.nu
use module.nu [filtered_modules]
# 注册钩子
def-env register_hook [
hook_type: string, # 钩子类型
module: string, # 模块名称
hook: closure, # 运行块
] {
log debug $"注册 ($hook_type): ($module) : ($hook)"
let old_hooks = (global_conf get_config $module $hook_type null)
# 合并新映射并设置
global_conf set_config $module $hook_type (
$old_hooks | append $hook
)
}
# 注册模块安装前运行钩子
export def-env register_pre_install [module: string, hook: closure] {
register_hook "pre_install" $module $hook
}
# 注册模块安装后运行钩子
export def-env register_post_install [module: string, hook: closure] {
register_hook "post_install" $module $hook
}
# 注册模块卸载前运行钩子
export def-env register_pre_uninstall [module: string, hook: block] {
register_hook "pre_uninstall" $module $hook
}
# 注册模块卸载后运行钩子
export def-env register_post_uninstall [module: string, hook: block] {
register_hook "post_uninstall" $module $hook
}
# 声明包依赖
export def-env require_packages [
module: string,
pkgs: list,
] {
log debug $"依赖: ($module): ($pkgs)"
register_hook "install_dep" $module {||
for $pkg_desc in $pkgs {
let result = (pkg resolve_manager $pkg_desc)
if ($result == null) {
log debug $"包描述符 ($pkg_desc) 无可用的包管理器,跳过安装"
continue
}
log debug $"解析包描述符:($pkg_desc) --> ($result)"
pkg install --manager=($result.manager) $result.pkg
}
}
register_hook "uninstall_dep" $module {||
let remove = (confirm "- 是否卸载已安装的依赖包?" "n")
if ($remove) {
for $pkg in $pkgs {
pkg uninstall $pkg
}
}
}
}
# 运行指定钩子
export def do_hook [
modules: list,
hook_type: string, # 钩子类型
hook_name: string, # 钩子名称
] {
# 遍历模块
for $module in (filtered_modules $modules) {
let hooks = (global_conf get_config $module $hook_type)
if ($hooks != null) {
log status $"运行 ($module) 模块的($hook_name)..."
for $hook in $hooks {
do $hook
null
}
log info ""
}
}
}

14
knotfiles/dotfile/mod.nu Normal file
View File

@ -0,0 +1,14 @@
# mod.nu -- dotfiles 配置的定义与管理相关 API
#
# Copyright (C) 2022-2023 KAAAsS
# 导出相关环境变量
export-env {
use global_conf.nu
use ../pkg.nu
}
export use symlink.nu *
export use hook.nu *
export use constraint.nu *
export use module.nu *

View File

@ -0,0 +1,56 @@
# module.nu -- 模块管理
#
# Copyright (C) 2022-2023 KAAAsS
use ../log.nu
use global_conf.nu
use constraint.nu [check_constraint]
# 获得筛选后的模块列表
export def filtered_modules [
want_modules?: list
] {
let modules = (global_conf modules).name
# 根据需要模块筛选
let modules = if ($want_modules | is-empty) {
$modules
} else {
# 检查模块是否存在
mut result = null
for $module in $want_modules {
if (not ($module in $modules)) {
log error $"模块 ($module) 不存在!"
exit 1
}
$result = ($result | append $module)
}
$result
}
# 进行约束检查
# TODO: 缓存检查结果
if ("IGNORE_CONSTRAINT" in $env and $env.IGNORE_CONSTRAINT) {
return $modules
}
mut result = null
for $module in $modules {
let constraints = (global_conf get_config $module "constraints" [])
let check = (
$constraints
| each { |it|
let result = (check_constraint $it)
if (not $result) {
log debug $"模块 ($module) 不满足约束 ($it)"
}
$result
}
| reduce -f true { |it, acc| $it and $acc }
)
if ($check) {
$result = ($result | append $module)
}
}
return $result
}

View File

@ -0,0 +1,180 @@
# symlink.nu -- 软链接管理
#
# Copyright (C) 2022-2023 KAAAsS
use ../log.nu
use ../interactive.nu [confirm]
use global_conf.nu
use module.nu [filtered_modules]
# 获得链接的目标目录或目标文件的父目录
def get_base_dir [local: path, dest: path] {
let type = ($local | path type)
if ($type == dir) {
$dest
} else {
$dest | path dirname
}
}
# 声明文件及其目标路径
export def-env declare [
module: string, # 模块名称
local: path, # 本地路径,以项目根目录为起点的相对路径
dest: string, # 目标路径,以用户家目录为起点的相对路径
--local-only: bool, # 是否是本地特有的文件。若文件不存在则会自动从 example 创建本地文件
] {
let type = ($local | describe)
let locals = if ($type == "list<string>") {
$local
} else {
[$local]
}
# 遍历 local 路径,获得映射
mut new_maps = null
for $local in $locals {
let dest = if ($type == "list<string>") {
# 对于文件列表(如 glob把文件名追加在最后
$dest | path join ($local | path basename)
} else {
$dest
}
log debug $"声明文件: ($module) : ($local) --> ($dest)"
$new_maps = ($new_maps | append [ { local: $local, dest: $dest } ])
# 若本地文件不存在,则根据模板创建本地文件
if ($local_only and (not ($local | path exists))) {
let template = $"($local).example"
if ($template | path exists) {
log debug $"创建本地文件: ($local)"
mkdir ($local | path dirname)
cp $template $local
} else {
error make {
msg: $"本地文件 ($local) 的模板不存在!配置有误!",
}
}
}
}
# 合并新映射并设置
let old_map = (global_conf get_config $module "file_map" null)
global_conf set_config $module "file_map" (
$old_map | append $new_maps
)
}
# 列出模块的文件信息
export def list_files [
modules?: list
] {
# 获取模块列表
let modules = (filtered_modules $modules)
# 增加文件信息
$modules | each { |$module|
let file_map = (global_conf get_config $module "file_map")
if ($file_map != null) {
# dest 相对路径转为绝对
let file_map = (
$file_map | each { |$it|
{
local: $it.local,
dest: ($env.HOME_DIR | path join $it.dest),
}
}
)
# 增加模块名列
[ [ module, v ]; [ $module, $file_map ] ]
}
} | flatten | flatten --all
}
# 进行模块文件的符号链接
export def do_link [
modules?: list
] {
let module_files = (
list_files $modules | group-by module | transpose module files
)
# 遍历模块
for $it in $module_files {
let module = $it.module
let files = $it.files
log status $"链接模块 ($module)..."
# 遍历文件映射,执行软链接
for $it in $files {
let local = $it.local
let dest = $it.dest
let type = ($dest | path type)
if ($type == symlink) {
log info $"($dest) 已链接,跳过"
} else {
let link = if ($type != "") {
# 文件存在时进行询问
let agree = (confirm $"- 文件 ($dest) 已存在,是否强制覆盖?" "n")
if ($agree) {
log debug $"删除 ($dest)"
rm -rf $dest
true
} else { false }
} else {
# 不存在则直接链接
true
}
# 进行符号链接
if ($link) {
log info $"链接 ($local) --> ($dest)"
mkdir (get_base_dir $local $dest)
ln -s $local $dest
}
}
}
log info ""
}
}
# 删除模块文件的符号链接
export def do_unlink [
modules?: list
] {
let module_files = (
list_files $modules | group-by module | transpose module files
)
# 遍历模块
for $it in $module_files {
let module = $it.module
let files = $it.files
log status $"清理模块 ($module)..."
# 遍历文件映射,删除软链接
for $it in $files {
let dest = $it.dest
let type = ($dest | path type)
if ($type == symlink) {
log info $"清理 ($dest)"
rm -rf $dest
} else if ($type != "") {
log warn $"- ($dest) 非符号链接,跳过"
}
null
}
log info ""
}
}

74
knotfiles/interactive.nu Normal file
View File

@ -0,0 +1,74 @@
# interactive.nu -- 交互模块
#
# Copyright (C) 2022 KAAAsS
def-env confirm_loop [ prompt: string, default: string ] {
let-env result = if ($env.result == null) {
let got = (input $prompt)
if ($got == "y" or $got == "n") {
$got
} else if ($got == "" and $default != null) {
$default
} else {
null
}
} else {
$env.result
}
}
# 询问用户是否问题
export def confirm [
prompt: string, # 提示信息
default?: string, # 默认值y 或 n
] {
use log.nu
# 提示符
let indicator = if ($default == "y") {
"Y/n"
} else if ($default == "n") {
"y/N"
} else {
"y/n"
}
let prompt = $"($prompt) [($indicator)] "
if ('NO_CONFIRM' in $env and $env.NO_CONFIRM) {
print $"($prompt)($default)"
if ($default == null) {
log error $"必须手动确认!"
exit 1
}
$default == "y"
} else {
# 尝试获得结果
let-env result = null
# 最多尝试 5 次(因为 nushell 的限制暂时无法在循环里编辑环境)
confirm_loop $prompt $default
confirm_loop $prompt $default
confirm_loop $prompt $default
confirm_loop $prompt $default
confirm_loop $prompt $default
# 超过次数仍未成功
if ($env.result == null) {
log error "超过最大尝试次数!"
exit 1
}
$env.result == "y"
}
}
def main [] {
print "测试 confirm"
print (confirm "测试 1")
print (confirm "测试 2" "y")
print (confirm "测试 3" "n")
let-env NO_CONFIRM = true
print (confirm "测试 2" "y")
print (confirm "测试 3" "n")
print (confirm "测试 1")
}

56
knotfiles/log.nu Normal file
View File

@ -0,0 +1,56 @@
# log.nu -- 日志模块
#
# Copyright (C) 2022 KAAAsS
def level_color [level: int] {
{
0: "blue",
10: "white",
12: "white",
15: "green",
20: "yellow",
25: "red",
} | get ($level | into string)
}
export def main [
level: int,
s: string
] {
let log_level = if ('LOG_LEVEL' in $env) {
$env.LOG_LEVEL
} else {
10
}
if ($level >= $log_level) {
let color = (level_color $level)
print $"(ansi $color)($s)(ansi reset)"
}
}
export def debug [s: string] {
main 0 $s
}
export def info [s: string] {
main 10 (
# 非空则增加一个缩进
if ($s | is-empty) { "" } else { $"- ($s)" }
)
}
export def status [s: string] {
main 12 $s
}
export def succ [s: string] {
main 15 $s
}
export def warn [s: string] {
main 20 $s
}
export def error [s: string] {
main 25 $s
}

249
knotfiles/pkg.nu Normal file
View File

@ -0,0 +1,249 @@
# pkg.nu -- 包管理器的抽象
#
# Copyright (C) 2022 KAAAsS
use pkgs/pacman.nu
use pkgs/homebrew.nu
export-env {
# 所有可用的包管理器
let-env package_managers = [
(pacman pack),
(homebrew pack),
(fallback_manager)
]
# 计算默认包管理器
let-env default_package_manager = (select_managers)
}
def select_managers [
manager?: string,
--allow-unavailable: bool
] {
if ($manager == null) {
# 选择第一个可用的包管理器
for $it in $env.package_managers {
let checker = ($it | get "available?")
if (do $checker) {
return $it
}
}
error make {
msg: "没有可用的包管理器"
}
} else {
# 选择对应名称的包管理器
let ret = ($env.package_managers | filter {|it|
($it.name == $manager)
})
# 未找到
if ($ret | is-empty) {
error make {
msg: $"未找到名称为 ($manager) 的包管理器"
}
}
# 检查是否可用
let ret = ($ret | get 0)
let checker = ($ret | get "available?")
if (not (do $checker)) {
if ($allow_unavailable) {
return null
} else {
error make {
msg: $"包管理器 ($manager) 不可用"
}
}
}
return $ret
}
}
# Fallback 包管理器,无法实际承担包管理功能,仅用于占位
def fallback_manager [] {
{
"name": "fallback",
"available?": {
log warn "无可用的包管理器!若安装包则会发生错误!"
true
},
"check_install?": {|p| false },
"install": {|p| false },
"uninstall": {|p| false }
}
}
# 检查包是否安装
export def check_install? [
pkg_name: string,
--manager: string
] {
let manager_impl = (select_managers $manager)
let func = ($manager_impl | get "check_install?")
do $func $pkg_name
}
# 安装包
export def install [
pkg_name: string,
--manager: string
] {
use log.nu
let span = (metadata $pkg_name).span;
if (check_install? --manager=$manager $pkg_name) {
log info $"包 ($pkg_name) 已经安装,跳过"
} else {
log info $"安装包 ($pkg_name)..."
# 安装包
let manager_impl = (select_managers $manager)
let func = ($manager_impl | get "install")
let ret = (do $func $pkg_name)
# 检查安装结果
if ($ret) {
log info $"成功安装包 ($pkg_name)"
} else {
error make {
msg: $"无法安装包 ($pkg_name)",
label: {
text: "包名",
start: $span.start,
end: $span.end
}
}
}
}
}
# 卸载包
export def uninstall [
pkg_name: string,
--manager: string
] {
use log.nu
let span = (metadata $pkg_name).span;
if (not (check_install? --manager=$manager $pkg_name)) {
log info $"未安装包 ($pkg_name),跳过"
} else {
log info $"卸载包 ($pkg_name)..."
# 卸载包
let manager_impl = (select_managers $manager)
let func = ($manager_impl | get "uninstall")
let ret = (do $func $pkg_name)
# 检查安装结果
if ($ret) {
log info $"成功卸载包 ($pkg_name)"
} else {
error make {
msg: $"无法卸载包 ($pkg_name)",
label: {
text: "包名",
start: $span.start,
end: $span.end
}
}
}
}
}
# 解析包对象对应的包管理器
export def resolve_manager [
pkg_desc
] {
let type = ($pkg_desc | describe)
if ($type == "string") {
# 未指定特殊包管理器,直接使用默认
return {
"pkg": $pkg_desc,
"manager": $env.default_package_manager.name
}
} else {
# 存在特殊包管理器配置,就遍历 key 直到找到可用的包管理器
for $expect_manager in ($pkg_desc | transpose key).key {
let manager_impl = (select_managers --allow-unavailable $expect_manager)
if ($manager_impl != null) {
return {
"pkg": ($pkg_desc | get $expect_manager),
"manager": $manager_impl.name
}
}
}
return null
}
}
export def test [] {
use testing *
# 增加 Mock 管理器
let-env package_managers = [
{
"name": "unavailable",
"available?": { false }
},
{
"name": "mock",
"available?": { true },
"check_install?": {|p| $p == "mock" or $p == "mockmock" },
"install": {|p| $p == "mock" },
"uninstall": {|p| $p == "mock" }
},
{
"name": "mock2",
"available?": { true },
"check_install?": {|p| $p == "mock2" },
}
]
let-env default_package_manager = (select_managers)
# select_managers
let mock = (select_managers "mock2")
assert ($mock.name == "mock2")
let mock = (select_managers)
assert ($mock.name == "mock")
assert error {
select_managers "unavailable"
}
# check_install?
assert (check_install? "mock")
assert (not (check_install? "mock2"))
assert (check_install? --manager=mock2 "mock2")
assert (not (check_install? --manager=mock2 "mock"))
# install
install "mock"
assert error {
install "mock2"
}
# uninstall
uninstall "mock"
uninstall "mock2" # 未安装
assert error {
uninstall "mockmock"
}
# resolve_manager
assert ((resolve_manager "test").manager == "mock")
assert ((resolve_manager {
"mock": "test2",
"mock2": "test3"
}).manager == "mock")
assert ((resolve_manager {
"mock2": "test3"
}).manager == "mock2")
assert error {
resolve_manager {
"mock3": "test3"
}
}
}

View File

@ -0,0 +1,48 @@
# homebrew.nu -- Homebrew 管理器的实际实现
#
# Copyright (C) 2022-2023 KAAAsS
# 检查包管理器是否可用
export def available? [] {
not (which brew | is-empty)
}
# 检查包是否安装
export def check_install? [
pkg_name: string
] {
let ret = (
do -i { brew list $pkg_name } | complete
)
$ret.exit_code == 0
}
# 安装包
export def install [
pkg_name: string
] {
let ret = (
do -i { brew install $pkg_name } | complete
)
$ret.exit_code == 0
}
# 卸载包
export def uninstall [
pkg_name: string
] {
let ret = (
do -i { brew uninstall $pkg_name } | complete
)
$ret.exit_code == 0
}
export def pack [] {
{
"name": "homebrew",
"available?": { available? },
"check_install?": {|p| check_install? $p },
"install": {|p| install $p },
"uninstall": {|p| uninstall $p }
}
}

48
knotfiles/pkgs/pacman.nu Normal file
View File

@ -0,0 +1,48 @@
# pacman.nu -- Pacman 管理器的实际实现
#
# Copyright (C) 2022-2023 KAAAsS
# 检查包管理器是否可用
export def available? [] {
not (which pacman | is-empty)
}
# 检查包是否安装
export def check_install? [
pkg_name: string
] {
let ret = (
do -i { pacman -Qi $pkg_name } | complete
)
$ret.exit_code == 0
}
# 安装包
export def install [
pkg_name: string
] {
let ret = (
do -i { sudo pacman -S --noconfirm $pkg_name } | complete
)
$ret.exit_code == 0
}
# 卸载包
export def uninstall [
pkg_name: string
] {
let ret = (
do -i { sudo pacman -R --noconfirm $pkg_name } | complete
)
$ret.exit_code == 0
}
export def pack [] {
{
"name": "pacman",
"available?": { available? },
"check_install?": {|p| check_install? $p },
"install": {|p| install $p },
"uninstall": {|p| uninstall $p }
}
}

33
tests/integrate_test Executable file
View File

@ -0,0 +1,33 @@
#!/usr/bin/env nu
# integrate_test -- 运行集成测试
#
# Copyright (C) 2022 KAAAsS
def main [] {
print "开始运行集成测试"
rm -rf work/home/
mkdir work/home
./dotfiles install -H work/home --less
./dotfiles sync -H work/home -v
./dotfiles uninstall -H work/home --no-confirm
}
# 运行安装功能的集成测试
def "main install" [] {
rm -rf work/home/
mkdir work/home
./dotfiles install -H work/home -v
}
# 运行同步功能的集成测试
def "main sync" [] {
./dotfiles sync -H work/home -v
}
# 运行卸载功能的集成测试
def "main uninstall" [] {
rm -rf work/home/
mkdir work/home
./dotfiles install -H work/home --less
./dotfiles uninstall -H work/home --no-confirm
}

24
tests/unit_test Executable file
View File

@ -0,0 +1,24 @@
#!/usr/bin/env nu
# test -- 运行单元测试
#
# Copyright (C) 2022 KAAAsS
def main [] {
print "开始运行全部单元测试"
main global_conf
main pkg
}
# 运行 dotfile 模块的单元测试
def "main global_conf" [] {
use ../knotfiles/dotfile/global_conf.nu
global_conf test
}
# 运行 pkg 模块的单元测试
def "main pkg" [] {
use ../knotfiles/pkg.nu
pkg test
}