From 7b2b01e022eab6201ba59e90dc3985462a387b98 Mon Sep 17 00:00:00 2001 From: yage Date: Mon, 21 Sep 2026 06:48:49 -0600 Subject: [PATCH] setup update complete --- .circleci/config.yml | 64 ++++ .docker/Dockerfile | 30 +- config/altaria/user/alacritty/local.toml | 0 config/altaria/user/i3/local.yaml | 0 config/altaria/user/polybar/local.ini | 0 setup/config.zsh | 170 ---------- setup/config/config.module.zsh | 25 ++ setup/config/env.zsh | 36 +++ .../config/generate-initial-local-config.zsh | 30 ++ setup/config/rc.zsh | 43 +++ setup/config/vim.zsh | 28 ++ setup/config/zsh.zsh | 22 ++ setup/filesystem/ensure-paths.zsh | 35 +++ setup/filesystem/filesystem.module.zsh | 17 + setup/{ => filesystem}/git.zsh | 39 +-- setup/filesystem/write-managed-block.zsh | 101 ++++++ setup/os-dependencies/arch.min.txt | 17 - setup/os-dependencies/arch.txt | 62 ---- setup/os-dependencies/debian.min.txt | 12 - setup/os-dependencies/debian.txt | 20 -- setup/os-dependencies/fedora.min.txt | 15 - setup/os-dependencies/fedora.txt | 51 --- setup/os-dependencies/generic.txt | 18 -- setup/os-dependencies/macos.min.txt | 14 - setup/os-dependencies/macos.txt | 48 --- setup/os.zsh | 293 ------------------ setup/packages/dependency.manifest.zsh | 154 +++++++++ setup/packages/dependency.zsh | 259 ++++++++++++++++ setup/packages/install-from-source.zsh | 61 ++++ setup/packages/installer.apt-get.zsh | 16 + setup/packages/installer.brew.zsh | 76 +++++ setup/packages/installer.dnf.zsh | 23 ++ setup/packages/installer.generic.zsh | 7 + setup/packages/installer.yay.zsh | 27 ++ setup/packages/packages.module.zsh | 29 ++ setup/prepare-zsh-utilities.zsh | 104 +++++++ setup/requirements.zsh | 63 ---- setup/run | 215 ++++++++++--- 38 files changed, 1376 insertions(+), 848 deletions(-) delete mode 100644 config/altaria/user/alacritty/local.toml delete mode 100644 config/altaria/user/i3/local.yaml delete mode 100644 config/altaria/user/polybar/local.ini delete mode 100644 setup/config.zsh create mode 100644 setup/config/config.module.zsh create mode 100644 setup/config/env.zsh create mode 100644 setup/config/generate-initial-local-config.zsh create mode 100644 setup/config/rc.zsh create mode 100644 setup/config/vim.zsh create mode 100644 setup/config/zsh.zsh create mode 100644 setup/filesystem/ensure-paths.zsh create mode 100644 setup/filesystem/filesystem.module.zsh rename setup/{ => filesystem}/git.zsh (64%) create mode 100644 setup/filesystem/write-managed-block.zsh delete mode 100644 setup/os-dependencies/arch.min.txt delete mode 100644 setup/os-dependencies/arch.txt delete mode 100644 setup/os-dependencies/debian.min.txt delete mode 100644 setup/os-dependencies/debian.txt delete mode 100644 setup/os-dependencies/fedora.min.txt delete mode 100644 setup/os-dependencies/fedora.txt delete mode 100644 setup/os-dependencies/generic.txt delete mode 100644 setup/os-dependencies/macos.min.txt delete mode 100644 setup/os-dependencies/macos.txt delete mode 100644 setup/os.zsh create mode 100644 setup/packages/dependency.manifest.zsh create mode 100644 setup/packages/dependency.zsh create mode 100644 setup/packages/install-from-source.zsh create mode 100644 setup/packages/installer.apt-get.zsh create mode 100644 setup/packages/installer.brew.zsh create mode 100644 setup/packages/installer.dnf.zsh create mode 100644 setup/packages/installer.generic.zsh create mode 100644 setup/packages/installer.yay.zsh create mode 100644 setup/packages/packages.module.zsh create mode 100644 setup/prepare-zsh-utilities.zsh delete mode 100644 setup/requirements.zsh diff --git a/.circleci/config.yml b/.circleci/config.yml index 36bd6aa..d4bd5cb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,6 +2,51 @@ version: 2.1 jobs: + test-setup: + parameters: + image: + type: string + # alpine has no supported installer, so setup can only *verify* + # dependencies there; unresolved ones are expected, not a failure + strict: + type: string + default: '--strict' + docker: + - image: << parameters.image >> + steps: + - run: + name: install test prerequisites + command: | + if command -v pacman >/dev/null 2>&1 + then pacman -Syu --noconfirm --needed git zsh sudo openssh tar gzip + elif command -v dnf >/dev/null 2>&1 + then dnf install -y git zsh sudo openssh-clients tar gzip + elif command -v apt-get >/dev/null 2>&1 + then apt-get update && apt-get install -y git zsh sudo openssh-client ca-certificates tar gzip + elif command -v apk >/dev/null 2>&1 + then apk add --no-cache git zsh sudo openssh-client shadow tar + fi + - checkout + - run: + name: run setup as an unprivileged user + command: | + useradd -m tester 2>/dev/null || adduser -D tester + echo 'tester ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/tester + + TESTER_HOME="$(getent passwd tester | cut -d: -f6)" + cp -a . "${TESTER_HOME}/.wryn" + chown -R tester "${TESTER_HOME}/.wryn" + + # a LOGIN shell, so /etc/profile contributes to PATH exactly as + # it would in a real session (debian puts /usr/games there) + su - tester -c "cd ~/.wryn && ./setup/run \ + --only packages \ + --min \ + --ci \ + << parameters.strict >> \ + --no-logfile \ + " + deploy: docker: - image: docker:git @@ -16,7 +61,26 @@ workflows: version: 2 main: jobs: + - test-setup: + name: test-arch + image: archlinux:base-devel + - test-setup: + name: test-fedora + image: fedora:latest + - test-setup: + name: test-debian + image: debian:stable + - test-setup: + name: test-alpine + image: alpine:latest + strict: '' + - deploy: + requires: + - test-arch + - test-fedora + - test-debian + - test-alpine context: - yage-docker filters: diff --git a/.docker/Dockerfile b/.docker/Dockerfile index a48cd5d..c67e9a6 100644 --- a/.docker/Dockerfile +++ b/.docker/Dockerfile @@ -13,15 +13,13 @@ RUN : \ git \ grep \ iproute2 \ + jo \ + jq \ ncurses \ ncurses-terminfo \ - nodejs \ - npm \ openssh \ pciutils \ - postgresql \ - py3-virtualenv \ - py3-nodeenv \ + postgresql-client \ redis \ ripgrep \ sed \ @@ -34,25 +32,37 @@ RUN : \ && apk add --update --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/ \ helm \ kubectl \ - && apk add --update --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \ && mkdir -p /root/.wryn \ ; +# alpine's newest yq (4.25) predates the '-r' flag scwrypts relies on, so +# take it from upstream releases instead +RUN : \ + && YQ_ARCH="$(uname -m)" \ + && case "${YQ_ARCH}" in \ + ( x86_64 ) YQ_ARCH=amd64 ;; \ + ( aarch64 ) YQ_ARCH=arm64 ;; \ + esac \ + && wget -qO /usr/local/bin/yq \ + "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${YQ_ARCH}" \ + && chmod +x /usr/local/bin/yq \ + && yq --version \ + ; + COPY . /root/.wryn RUN : \ && echo "#!/bin/zsh" > /bin/hostnamectl && echo "echo container" > /bin/hostnamectl && chmod +x /bin/hostnamectl \ && echo "#!/bin/zsh" > /bin/scwrypts && echo '/root/.wryn/zsh/plugins/scwrypts/scwrypts $@' >> /bin/scwrypts && chmod +x /bin/scwrypts \ - && mkdir /root/.wryn/bin/container \ + && mkdir -p /root/.wryn/config/local/container/bin \ && sed -i 's|/bin/ash|/bin/zsh|g' /etc/passwd \ - && /bin/zsh -c '/root/.wryn/setup/run --no-fluff --ci --force-root' \ + && /bin/zsh -c '/root/.wryn/setup/run --min --ci --force-root --no-logfile --skip config' \ + && echo 'source /root/.wryn/zsh/rc' > /root/.zshrc \ && echo 'source /root/.wryn/config/dotwryn.env.zsh' > /root/.config/wryn/env.zsh \ && echo "WELCOME() {}" >> /root/.config/wryn/env.zsh \ && echo "PS1_INDICATOR_SYMBOL=''" >> /root/.config/wryn/env.zsh \ && echo "PS1_USER=\"root@\$(ip a | grep inet | grep -v '127\\.0\\.0\.1' | sed 's|/| |g' | awk '{print \$2;}')\"" >> /root/.config/wryn/env.zsh \ && echo "source /root/.wryn/zsh/plugins/scwrypts/scwrypts.plugin.zsh" >> /root/.config/wryn/env.zsh \ - && rm /root/dotwryn-install.log \ - && scwrypts --name scwrypts/virtualenv/update-all --group scwrypts --type zsh \ ; ENTRYPOINT ["/root/.wryn/.docker/entrypoint"] diff --git a/config/altaria/user/alacritty/local.toml b/config/altaria/user/alacritty/local.toml deleted file mode 100644 index e69de29..0000000 diff --git a/config/altaria/user/i3/local.yaml b/config/altaria/user/i3/local.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/config/altaria/user/polybar/local.ini b/config/altaria/user/polybar/local.ini deleted file mode 100644 index e69de29..0000000 diff --git a/setup/config.zsh b/setup/config.zsh deleted file mode 100644 index 546f99b..0000000 --- a/setup/config.zsh +++ /dev/null @@ -1,170 +0,0 @@ -setup.config() { - echo.status 'starting application configuration' - - setup.config._.generate-initial-local-config - - setup.config._.zsh || return 2 - setup.config._.vim || return 3 - - setup.config._.system || return 4 - - SCWRYPTS generate i3 config || return 5 - - echo.success 'finished application configuration' -} - -##################################################################### - -setup.config._.generate-initial-local-config() { - local local_hostname="$(hostnamectl --static 2>/dev/null || hostname 2>/dev/null | sed 's/\.local$//')" - [ "${local_hostname}" ] || return 0 - - mkdir -p "${DOTWRYN_PATH}/bin/${local_hostname}" - - local env_dir="${DOTWRYN_PATH}/config/scwrypts/environments" - local group - for group in $(find "${env_dir}" -type f -name \*env.yaml | sed -n 's|.*/local\.\([^.]*\)\.env\.yaml|\1|p') - do - [ -f "${env_dir}/local.${local_hostname}.${group}.env.yaml" ] && continue - - if [ -f "${env_dir}/local.altaria.${group}.env.yaml" ] - then - cp "${env_dir}/local.altaria.${group}.env.yaml" "${env_dir}/local.${local_hostname}.${group}.env.yaml" - else - cp "${env_dir}/local.${group}.env.yaml" "${env_dir}/local.${local_hostname}.${group}.env.yaml" - fi - done - unset group - - local local_override - for local_override in \ - i3/local.yaml \ - polybar/local.ini \ - alacritty/local.toml \ - ; - do - mkdir -p -- "${DOTWRYN_PATH}/config/${local_hostname}/user/$(dirname -- "${local_override}")" - touch -- "${DOTWRYN_PATH}/config/${local_hostname}/user/${local_override}" - done - unset local_override -} - -##################################################################### - -setup.config._.zsh() { - setup.config._.env zsh || return 1 - setup.config._.rc zsh || return 2 - setup.config._.set-default-shell || return 3 -} - -setup.config._.set-default-shell() { - local default_shell="$(awk -F: -v user="${USER}" '$1 == user {print $NF}' /etc/passwd)" - [[ "${default_shell}" =~ zsh ]] && return 0 - - [[ "${FORCE_ROOT}" == true ]] && return 0 - - echo.status 'setting zsh as default shell' - sudo chsh -s "$(which zsh)" "$(whoami)" 2>&1 \ - && echo.success "set zsh as default shell for '${USER}'" \ - || utils.fail 1 'failed to set zsh as default shell' \ - ; -} - -##################################################################### - -setup.config._.vim() { - setup.config._.rc vim || return 1 - - echo.status 'starting vim setup' - SCWRYPTS dotwryn vundle update || return 2 - - setup.config._.vim.link-superuser-rc - - return 0 -} - -setup.config._.vim.link-superuser-rc() { - sudo [ -f /root/.vimrc ] && return 0 - - echo "let \$DOTWRYN=\"${DOTWRYN_PATH}\"\nsource \"${DOTWRYN_PATH}/vim/rc.vim\"" \ - | sudo tee /root/.vimrc >/dev/null - - sudo mkdir -p /root/.vim - sudo ln -s "${HOME}/.vim/bundle" /root/.vim/bundle -} - -##################################################################### - -setup.config._.system() { - SCWRYPTS dotwryn system setup -} - -##################################################################### - -setup.config._.env() { - local config_type="${1}" - local default_config="${DOTWRYN_PATH}/${config_type}/env.${config_type}" - local local_config="${HOME}/.config/wryn/env.${config_type}" - - [ -f "${local_config}" ] && { - case "${OVERWRITE_EXISTING}" in - ( false ) return 0 ;; - ( true ) - echo.warning "local ${config_type} configuration exists (${local_config})" - utils.yN 'overwrite this configuration?' || return 0 - - mv "${local_config}" "${local_config}.bak" >/dev/null 2>&1 \ - && echo.status "created backup of local configuration (${local_config}.bak)" - ;; - esac - } - - echo.status "setting up ${config_type} configuration (${local_config})" - - local comment - case "${config_type}" in - ( zsh ) comment='#' ;; - esac - - { - echo "source ${default_config}" - echo -e "\\n${comment}\n${comment} .wryn configuration overrides\n${comment}\n" - sed "s/^[^${comment}].*/${comment}&/" "${default_config}" - } > "${local_config}" \ - && echo.success "created ${config_type} configuration" \ - || utils.fail 1 "unable to create ${config_type} configuration" \ - ; - - EDITOR=vim VISUAL=vim utils.io.edit "${local_config}" -} - - -setup.config._.rc() { - local config_type="${1}" - local rc - case "${config_type}" in - ( vim ) - rc="${XDG_CONFIG_HOME:-${HOME}/.config}/vim/vimrc" - mkdir -p -- "$(dirname -- "${rc}")" - [ -f "${HOME}/.vimrc" ] \ - && cat "${HOME}/.vimrc" >> "${XDG_CONFIG_HOME:-${HOME}/.config}/vim/vimrc" \ - && rm "${HOME}/.vimrc" \ - ; - ;; - ( * ) rc="${HOME}/.${config_type}rc" ;; - esac - - local source_line - case "${config_type}" in - ( zsh ) source_line="source ${DOTWRYN_PATH}/zsh/rc" ;; - ( vim ) source_line="source ${DOTWRYN_PATH}/vim/rc.vim" ;; - esac - - grep -q "^${source_line}$" "${rc}" \ - || echo "${source_line}" >> "${rc}" - - grep -q "^${source_line}$" "${rc}" \ - && echo.success "${config_type}rc is configured correctly" \ - || utils.fail 1 "failed to configure ${config_type}rc" \ - ; -} diff --git a/setup/config/config.module.zsh b/setup/config/config.module.zsh new file mode 100644 index 0000000..9f5946b --- /dev/null +++ b/setup/config/config.module.zsh @@ -0,0 +1,25 @@ +##################################################################### + +for setup_library in "${0:a:h}"/*.zsh +do + case "${setup_library:t}" in + ( config.module.zsh ) ;; # self + ( * ) source "${setup_library}" ;; + esac +done +unset setup_library + +##################################################################### + +setup.config() { : \ + && echo.status 'starting application configuration' \ + && : \ + && setup.config.generate-initial-local-config \ + && setup.config.zsh \ + && setup.config.vim \ + && setup.scwrypts dotwryn system setup \ + && setup.scwrypts generate i3 config \ + && : \ + && echo.success 'finished application configuration' \ + || echo.error 'application configuration failed (see above)' \ +; } diff --git a/setup/config/env.zsh b/setup/config/env.zsh new file mode 100644 index 0000000..d5cab08 --- /dev/null +++ b/setup/config/env.zsh @@ -0,0 +1,36 @@ +##################################################################### + +setup.config.env() { + local config_type="${1}" + local default_config="${DOTWRYN_SETUP__DOTWRYN}/${config_type}/env.${config_type}" + local local_config="${HOME}/.config/wryn/env.${config_type}" + + if [[ -f "${local_config}" ]] + then + case "${DOTWRYN_SETUP__OVERWRITE_EXISTING}" in + ( false ) return 0 ;; + ( true ) + echo.warning "local ${config_type} configuration exists (${local_config})" + utils.yN 'overwrite this configuration?' || return 0 + + mv -- "${local_config}" "${local_config}.bak" &>/dev/null \ + && echo.status "created backup of local configuration (${local_config}.bak)" + ;; + esac + fi + + echo.status "setting up ${config_type} configuration (${local_config})" + + local comment='#' + + { + echo "source ${default_config}" + echo -e "\\n${comment}\n${comment} .wryn configuration overrides\n${comment}\n" + sed "s/^[^${comment}].*/${comment}&/" "${default_config}" + } > "${local_config}" \ + && echo.success "created ${config_type} configuration" \ + || echo.error "unable to create ${config_type} configuration" \ + || return 1 + + EDITOR=vim VISUAL=vim utils.io.edit "${local_config}" +} diff --git a/setup/config/generate-initial-local-config.zsh b/setup/config/generate-initial-local-config.zsh new file mode 100644 index 0000000..f17346c --- /dev/null +++ b/setup/config/generate-initial-local-config.zsh @@ -0,0 +1,30 @@ +##################################################################### + +setup.config.generate-initial-local-config() { + local local_hostname="$(hostnamectl --static 2>/dev/null || hostname 2>/dev/null | sed 's/\.local$//')" + [[ "${local_hostname}" ]] || return 0 + + mkdir -p -- "${DOTWRYN_SETUP__DOTWRYN}/config/local/${local_hostname}/bin" + + local env_dir="${DOTWRYN_SETUP__DOTWRYN}/config/scwrypts/environments" + local group + for group in $(find "${env_dir}" -type f -name \*env.yaml | sed -n 's|.*/local\.\([^.]*\)\.env\.yaml|\1|p') + do + [[ -f "${env_dir}/local.${local_hostname}.${group}.env.yaml" ]] && continue + + cp -- "${env_dir}/local.${group}.env.yaml" "${env_dir}/local.${local_hostname}.${group}.env.yaml" + done + unset group + + local local_override + for local_override in \ + i3/local.yaml \ + polybar/local.ini \ + alacritty/local.toml \ + ; + do + mkdir -p -- "${DOTWRYN_SETUP__DOTWRYN}/config/local/${local_hostname}/user/$(dirname -- "${local_override}")" + touch -- "${DOTWRYN_SETUP__DOTWRYN}/config/local/${local_hostname}/user/${local_override}" + done + unset local_override +} diff --git a/setup/config/rc.zsh b/setup/config/rc.zsh new file mode 100644 index 0000000..c6ed15b --- /dev/null +++ b/setup/config/rc.zsh @@ -0,0 +1,43 @@ +##################################################################### + +setup.config.rc() { + local config_type="${1}" + local rc + case "${config_type}" in + ( vim ) + rc="${XDG_CONFIG_HOME:-${HOME}/.config}/vim/vimrc" + mkdir -p -- "$(dirname -- "${rc}")" + [[ -f "${HOME}/.vimrc" ]] \ + && cat "${HOME}/.vimrc" >> "${rc}" \ + && rm -- "${HOME}/.vimrc" \ + ; + ;; + ( * ) rc="${HOME}/.${config_type}rc" ;; + esac + touch -- "${rc}" + + local source_line comment + case "${config_type}" in + ( zsh ) + source_line="source ${DOTWRYN_SETUP__DOTWRYN}/zsh/rc" + comment='#' + ;; + ( vim ) + source_line="source ${DOTWRYN_SETUP__DOTWRYN}/vim/rc.vim" + comment='"' + ;; + ( * ) + echo.error "no rc source-line defined for '${config_type}'" + return 1 + ;; + esac + + setup.filesystem.write-managed-block "${rc}" --comment "${comment}" --tag dotwryn-rc --one-line <<-EOF + ${source_line} + EOF + + grep -qF -- "${source_line}" "${rc}" \ + && echo.success "${config_type}rc is configured correctly" \ + || echo.error "failed to configure ${config_type}rc" \ + || return 1 +} diff --git a/setup/config/vim.zsh b/setup/config/vim.zsh new file mode 100644 index 0000000..bdb6121 --- /dev/null +++ b/setup/config/vim.zsh @@ -0,0 +1,28 @@ +##################################################################### + +setup.config.vim() { : \ + && echo.status 'configuring vim rc' \ + && setup.config.rc vim \ + && : \ + && echo.status 'starting vim setup' \ + && setup.scwrypts dotwryn vundle update \ + && setup.config.vim._.link-superuser-rc \ +; } + +setup.config.vim._.link-superuser-rc() { + # already root (a container build) needs no sudo, and a machine + # without sudo at all can't get one + local sudo=sudo + [[ "${EUID}" -eq 0 ]] && sudo='' + [[ -n "${sudo}" ]] && ! command -v sudo &>/dev/null \ + && echo.warning 'sudo unavailable; skipping superuser vimrc' \ + && return 0 + + ${sudo} test -f /root/.vimrc && return 0 + + echo "let \$DOTWRYN=\"${DOTWRYN_SETUP__DOTWRYN}\"\nsource \"${DOTWRYN_SETUP__DOTWRYN}/vim/rc.vim\"" \ + | ${sudo} tee /root/.vimrc >/dev/null + + ${sudo} mkdir -p -- /root/.vim + ${sudo} ln -s -- "${HOME}/.vim/bundle" /root/.vim/bundle +} diff --git a/setup/config/zsh.zsh b/setup/config/zsh.zsh new file mode 100644 index 0000000..8ef655c --- /dev/null +++ b/setup/config/zsh.zsh @@ -0,0 +1,22 @@ +##################################################################### + +setup.config.zsh() { : \ + && setup.config.env zsh \ + && setup.config.rc zsh \ + && setup.config._.set-default-shell \ +; } + +setup.config._.set-default-shell() { + local default_shell="$(awk -F: -v user="${USER}" '$1 == user {print $NF}' /etc/passwd)" + [[ "${default_shell}" =~ zsh ]] && return 0 + + [[ "${DOTWRYN_SETUP__FORCE_ROOT}" == true ]] && return 0 + + : \ + && echo.status 'setting zsh as default shell' \ + && sudo chsh -s "$(command -v zsh)" "${USER}" 2>&1 \ + && echo.success "set zsh as default shell for '${USER}'" \ + || echo.error 'failed to set zsh as default shell' \ + || return 1 \ + ; +} diff --git a/setup/filesystem/ensure-paths.zsh b/setup/filesystem/ensure-paths.zsh new file mode 100644 index 0000000..df0a3fd --- /dev/null +++ b/setup/filesystem/ensure-paths.zsh @@ -0,0 +1,35 @@ +##################################################################### + +setup.filesystem.ensure-paths() { + local ERRORS=0 + local directories=( + "${XDG_CONFIG_HOME:-${HOME}/.config}/wryn" + "${HOME}/.local/bin" + ) + + local files=( + "${HOME}/.zshrc" + ) + + echo.status 'ensuring required paths exist' + local directory + for directory in "${directories[@]}" + do + mkdir -p -- "${directory}" || echo.error "failed to create directory '${directory}'" + done + unset directory + + local file + for file in "${files[@]}" + do + touch -- "${file}" || echo.error "failed to create file '${file}'" + done + unset file + + [[ "${ERRORS}" -eq 0 ]] \ + && echo.success 'required paths ready' \ + || echo.error 'failed to ensure required paths' \ + ; + + return "${ERRORS}" +} diff --git a/setup/filesystem/filesystem.module.zsh b/setup/filesystem/filesystem.module.zsh new file mode 100644 index 0000000..10db58e --- /dev/null +++ b/setup/filesystem/filesystem.module.zsh @@ -0,0 +1,17 @@ +##################################################################### + +for setup_library in "${0:a:h}"/*.zsh +do + case "${setup_library:t}" in + ( filesystem.module.zsh ) ;; # self + ( * ) source "${setup_library}" ;; + esac +done +unset setup_library + +##################################################################### + +setup.filesystem() { : \ + && setup.filesystem.ensure-paths \ + && setup.filesystem.git \ +; } diff --git a/setup/git.zsh b/setup/filesystem/git.zsh similarity index 64% rename from setup/git.zsh rename to setup/filesystem/git.zsh index 8194a03..722cc7f 100644 --- a/setup/git.zsh +++ b/setup/filesystem/git.zsh @@ -1,41 +1,42 @@ ##################################################################### -setup.git() { +setup.filesystem.git() { local sources_dir="${XDG_DATA_HOME:-${HOME}/.local/share}/project-source-code" - SOURCE_DIR="${DOTWRYN_PATH}" \ + SOURCE_DIR="${DOTWRYN_SETUP__DOTWRYN}" \ TARGET_DIR="${sources_dir}/yage/dotwryn" \ REMOTE_UPSTREAMS=( 'git@github.com:wrynegade/dotwryn.git' 'git@yage.io:wrynegade/dotwryn.git' 'git@bitbucket.org:wrynegade/dotwryn.git' - ) setup.git._.remotes '.wryn' + ) setup.filesystem.git._.remotes '.wryn' - SOURCE_DIR="${DOTWRYN_PATH}/zsh/plugins/code-activator" \ + SOURCE_DIR="${DOTWRYN_SETUP__DOTWRYN}/zsh/plugins/code-activator" \ TARGET_DIR="${sources_dir}/zsh/code-activator" \ REMOTE_UPSTREAMS=( 'git@yage.io:zsh/code-activator.git' 'git@github.com:wrynegade/code-activator.git' - ) setup.git._.remotes 'zsh-plugins/code-activator' + ) setup.filesystem.git._.remotes 'zsh-plugins/code-activator' - SOURCE_DIR="${DOTWRYN_PATH}/zsh/plugins/scwrypts" \ + SOURCE_DIR="${DOTWRYN_SETUP__DOTWRYN}/zsh/plugins/scwrypts" \ TARGET_DIR="${sources_dir}/zsh/scwrypts" \ REMOTE_UPSTREAMS=( 'git@yage.io:zsh/scwrypts' 'git@github.com:wrynegade/scwrypts' - ) setup.git._.remotes 'zsh-plugins/scwrypts' + ) setup.filesystem.git._.remotes 'zsh-plugins/scwrypts' return 0 } -setup.git._.remotes() { +setup.filesystem.git._.remotes() { : \ - && [ "${SOURCE_DIR}" ] \ - && [ "${TARGET_DIR}" ] \ - && [[ ${#REMOTE_UPSTREAMS[@]} -gt 0 ]] \ - || return 1 + && [[ "${SOURCE_DIR}" ]] \ + && [[ "${TARGET_DIR}" ]] \ + && [[ "${#REMOTE_UPSTREAMS[@]}" -gt 0 ]] \ + || return 1 \ + ; - [ "$1" ] && echo.status "updating remotes for '$1'" + [[ "${1}" ]] && echo.status "updating remotes for '${1}'" git -C "${SOURCE_DIR}" remote rm upstream 2>/dev/null git -C "${SOURCE_DIR}" remote add upstream "${REMOTE_UPSTREAMS[1]}" @@ -62,19 +63,19 @@ setup.git._.remotes() { done unset remote_upstream - SOURCE_DIR="${SOURCE_DIR}" TARGET_DIR="${TARGET_DIR}" setup.git._.link-to-projects \ + SOURCE_DIR="${SOURCE_DIR}" TARGET_DIR="${TARGET_DIR}" setup.filesystem.git._.link-to-projects \ || echo.warning "failed to link '${TARGET_DIR}'" return 0 } -setup.git._.link-to-projects() { - [ "${TARGET_DIR}" ] && [ "${SOURCE_DIR}" ] \ +setup.filesystem.git._.link-to-projects() { + [[ "${TARGET_DIR}" ]] && [[ "${SOURCE_DIR}" ]] \ || return 1 { - mkdir -p "${TARGET_DIR}" - rm "${TARGET_DIR}/code" - ln -s "${SOURCE_DIR}" "${TARGET_DIR}/code" + mkdir -p -- "${TARGET_DIR}" + rm -- "${TARGET_DIR}/code" + ln -s -- "${SOURCE_DIR}" "${TARGET_DIR}/code" } &>/dev/null } diff --git a/setup/filesystem/write-managed-block.zsh b/setup/filesystem/write-managed-block.zsh new file mode 100644 index 0000000..0efec7e --- /dev/null +++ b/setup/filesystem/write-managed-block.zsh @@ -0,0 +1,101 @@ +##################################################################### + +setup.filesystem.write-managed-block() { + # replaces (or appends) a dotwryn-managed block in a file, taking the + # block body from stdin, so that every run refreshes the block rather + # than appending another copy of it + # + # callers should use a "<<-" heredoc: it strips leading TABS (letting + # the body stay indented with its caller) but leaves SPACES alone, so + # indent the block's own nesting with spaces to keep it in the output + # + # "--one-line" trades the begin/end banner for a trailing comment on + # the line itself, which suits a one-line entry like an rc source + local file tag + local comment='#' + local one_line=false + + local _s _p + while [[ "${#}" -gt 0 ]] + do + _s=1 + case "${1}" in + ( --comment ) _s=2 + comment="${2}" + ;; + ( --tag ) _s=2 + tag="${2}" + ;; + ( --one-line ) + one_line=true + ;; + ( * ) + case "$((_p+=1))" in + ( 1 ) file="${1}" ;; + ( * ) echo.error "write-managed-block: unexpected argument '${1}'" ;; + esac + ;; + esac + shift "${_s}" + done + unset _s _p + + [[ "${file}" ]] \ + || echo.error 'write-managed-block: no file given' \ + || return 1 + + # a single file can carry several managed blocks, so each one has to be + # named to know which of them it owns + [[ "${tag}" ]] \ + || echo.error "write-managed-block: no --tag given for '${file}'" \ + || return 1 + + local body="$(cat)" + + [[ "${one_line}" == false ]] || [[ "${body}" != *$'\n'* ]] \ + || echo.error "write-managed-block: --one-line needs a single-line body for '${file}'" \ + || return 1 + + mkdir -p -- "${file:h}" + touch -- "${file}" + + local -a lines block + lines=("${(@f)$(<"${file}")}") + [[ -s "${file}" ]] || lines=() + + # an existing block is replaced where it sits, so anything the user + # wrote around it keeps its position + local begin_index end_index + case "${one_line}" in + ( true ) + local marker="${comment} managed by dotwryn setup (${tag})" + block=("${body} ${marker}") + + # "(b)" keeps the tag's parentheses literal rather than glob + begin_index="${lines[(i)*${(b)marker}]}" + end_index="${begin_index}" + ;; + ( false ) + local begin="${comment} >>> managed by dotwryn setup (${tag}) >>>" + local end="${comment} <<< managed by dotwryn setup (${tag}) <<<" + block=("${begin}" "${(@f)body}" "${end}") + + begin_index="${lines[(ie)${begin}]}" + end_index="${lines[(ie)${end}]}" + [[ "${end_index}" -le "${#lines[@]}" ]] || end_index="${#lines[@]}" + ;; + esac + + if [[ "${begin_index}" -gt "${#lines[@]}" ]] + then + lines+=("${block[@]}") + else + lines=( + "${lines[@]:0:$((begin_index - 1))}" + "${block[@]}" + "${lines[@]:${end_index}}" + ) + fi + + print -l -- "${lines[@]}" > "${file}" +} diff --git a/setup/os-dependencies/arch.min.txt b/setup/os-dependencies/arch.min.txt deleted file mode 100644 index 1eceef1..0000000 --- a/setup/os-dependencies/arch.min.txt +++ /dev/null @@ -1,17 +0,0 @@ -base-devel -bat -cmake -cowsay -figlet -fzf -go-yq -htop -jo -jq -lolcat -ncurses -python -ripgrep -scwrypts -tmux -vim diff --git a/setup/os-dependencies/arch.txt b/setup/os-dependencies/arch.txt deleted file mode 100644 index f9cfd0a..0000000 --- a/setup/os-dependencies/arch.txt +++ /dev/null @@ -1,62 +0,0 @@ -1password -1password-cli -alacritty -arch-install-scripts -aws-cli-v2 -base-devel -bat -cmake -cowsay -feh -figlet -flameshot -fprintd -fzf -go -go-yq -google-chrome -htop -i3-wm -jdk-openjdk -jo -jq -lolcat -lxappearance -mono -ncurses -fastfetch -nodejs -noto-fonts-emoji -npm -ntp -obs-studio -openssh -otf-monaspace -pam-fprint-grosshack -picom -playerctl -polybar -python -python-pip -python-pylint -python-rtmidi -python-virtualenv -rage-encryption -ripgrep -rofi -rustup -scwrypts -sudo -tmux -ttf-nerd-fonts-symbols -unipicker -vi -vim -vim-runtime -which -xclip -xdotool -yamllint -zip -zsh -zsh-completions diff --git a/setup/os-dependencies/debian.min.txt b/setup/os-dependencies/debian.min.txt deleted file mode 100644 index 3aa2d6c..0000000 --- a/setup/os-dependencies/debian.min.txt +++ /dev/null @@ -1,12 +0,0 @@ -bsdextrautils -build-essential -cmake -cowsay -figlet -fzf -jo -jq -ncurses-dev -ripgrep -tmux -uuid-runtime diff --git a/setup/os-dependencies/debian.txt b/setup/os-dependencies/debian.txt deleted file mode 100644 index dd58ca3..0000000 --- a/setup/os-dependencies/debian.txt +++ /dev/null @@ -1,20 +0,0 @@ -awscli -bsdextrautils -build-essential -cmake -cowsay -default-jdk -figlet -fzf -golang -jo -jq -lolcat -mono-devel -ncurses-dev -nodejs -npm -python3-dev -ripgrep -tmux -uuid-runtime diff --git a/setup/os-dependencies/fedora.min.txt b/setup/os-dependencies/fedora.min.txt deleted file mode 100644 index 5e8eeb8..0000000 --- a/setup/os-dependencies/fedora.min.txt +++ /dev/null @@ -1,15 +0,0 @@ -bat -cmake -cowsay -figlet -fzf -htop -jo -jq -lolcat -ncurses -python3-devel -ripgrep -tmux -vim-enhanced -yq diff --git a/setup/os-dependencies/fedora.txt b/setup/os-dependencies/fedora.txt deleted file mode 100644 index fa4e1d3..0000000 --- a/setup/os-dependencies/fedora.txt +++ /dev/null @@ -1,51 +0,0 @@ -autoconf -automake -bat -binutils -bison -cmake -containerd.io -cowsay -docker-ce -docker-ce-cli -feh -figlet -file -findutils -flex -fzf -gcc -gcc-c++ -gettext -gnupg2 -go -htop -java-latest-openjdk -jo -jq -libtool -lolcat -mono-complete -ncurses -fastfetch -nodejs -npm -openssh -patch -pkg-config -python3-devel -python3-pip -python3-pylint -python3-virtualenv -redis -ripgrep -rust -sed -sudo -tmux -vi -vim-enhanced -yamllint -yq -zip -zsh diff --git a/setup/os-dependencies/generic.txt b/setup/os-dependencies/generic.txt deleted file mode 100644 index 1d7ffc6..0000000 --- a/setup/os-dependencies/generic.txt +++ /dev/null @@ -1,18 +0,0 @@ -aws -cmake -cowsay -figlet -fzf -go -java -jo -jq -lolcat -ncurses -node -npm -python3 -rg -tmux -xbuild -yq diff --git a/setup/os-dependencies/macos.min.txt b/setup/os-dependencies/macos.min.txt deleted file mode 100644 index 349e8b2..0000000 --- a/setup/os-dependencies/macos.min.txt +++ /dev/null @@ -1,14 +0,0 @@ -bat -cmake -cowsay -figlet -fzf -htop -jo -jq -lolcat -ncurses -python -ripgrep -tmux -vim diff --git a/setup/os-dependencies/macos.txt b/setup/os-dependencies/macos.txt deleted file mode 100644 index 7d53bc0..0000000 --- a/setup/os-dependencies/macos.txt +++ /dev/null @@ -1,48 +0,0 @@ -autoconf -automake -bat -binutils -bison -cmake -coreutils -cowsay -fastfetch -feh -figlet -findutils -flex -fzf -gawk -gcc -gettext -go -gpatch -gpg -grep -gsed -htop -iproute2mac -jo -jq -libtool -lolcat -mono -ncurses -node -npm -openjdk -openssh -pkg-config -pylint -python -python-setuptools -redis -ripgrep -rust -tmux -vim -virtualenv -yamllint -yq -zip -zsh diff --git a/setup/os.zsh b/setup/os.zsh deleted file mode 100644 index e91038f..0000000 --- a/setup/os.zsh +++ /dev/null @@ -1,293 +0,0 @@ -##################################################################### - -setup.os() { - setup.os._.make-required-resources || return 1 - [[ "${CI}" == true ]] && { echo.status 'detected CI; skipping os setup'; return 0; } - utils.io.getsudo - - local OS_NAME="$(setup.os._.get-os)" - [ ! "${OS_NAME}" ] && utils.abort - - setup.os._.install-source-dependencies || return 2 - setup.os._.install-managed-dependencies || return 3 -} - -setup.os._.get-os() { - local OS_NAME="$(lsb_release -is 2>/dev/null | tr '[:upper:]' '[:lower:]')" - - [ ! "${OS_NAME}" ] \ - && OS_NAME="$(sed -n 's/^ID=//p' /etc/os-release 2>/dev/null)" - - uname -s | grep -q Darwin \ - && OS_NAME=macos - - [ ! "${OS_NAME}" ] \ - && echo.warning 'failed to detect operating system' \ - && OS_NAME="$(echo -e "arch\ndebian\nother" | utils.fzf 'select an operating system')" \ - ; - - [[ "${OS_NAME}" =~ ^ubuntu$ ]] && OS_NAME=debian - - [[ "${OS_NAME}" =~ ^[Ee]ndeavour[Oo][Ss]$ ]] && OS_NAME=arch - - echo "${OS_NAME}" -} - -##################################################################### - -setup.os._.install-from-source() { # clone + build/install a git-hosted package (AUR PKGBUILD or Makefile) - local target_url="${1}" - local name="${2:-$(echo "${target_url}" | sed 's/.*\///; s/\.git$//')}" - local install_dir="${HOME}/.local/share/source-packages" - mkdir -p "${install_dir}" - - if [ -d "${install_dir}/${name}" ] - then - echo.status "updating '${name}'" - ( cd "${install_dir}/${name}" && git pull origin "$(git rev-parse --abbrev-ref HEAD)" ) \ - && echo.success "successfully updated '${name}'" \ - || { echo.error "failed to update '${name}'"; return 1; } - else - echo.status "downloading '${name}'" - git clone "${target_url}" "${install_dir}/${name}" \ - && echo.success "successfully downloaded '${name}'" \ - || { echo.error "failed to download '${name}'"; return 2; } - fi - - cd "${install_dir}/${name}" - if [ -f ./PKGBUILD ] - then - echo.status "installing '${name}'" - yes | makepkg -si \ - && echo.success "successfully installed '${name}'" \ - || { echo.error "failed to install '${name}'"; return 3; } - elif [ -f ./Makefile ] - then - echo.status "building '${name}'" - make \ - && echo.success "finished building '${name}'" \ - || { echo.error "build failed for '${name}'"; return 4; } - - echo.status "installing '${name}'" - utils.io.getsudo - sudo make install \ - && echo.success "successfully installed '${name}'" \ - || { echo.error "failed to install '${name}'"; return 5; } - else - echo.warning 'could not detect supported installation method' - echo.reminder "complete manual installation in the directory below:" - echo.reminder "${install_dir}/${name}" - fi -} - -setup.os._.install-source-dependencies() { - case "${OS_NAME}" in - ( arch ) - command -v yay >/dev/null 2>&1 \ - || setup.os._.install-from-source 'https://aur.archlinux.org/yay.git' 'yay' \ - ; - ;; - - ( fedora ) ;; - - ( debian ) ;; - - ( macos ) - xcode-select -p &>/dev/null || { # brew requires the xcode command line tools - echo.status 'installing xcode command line tools' - touch /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress - local clt_package="$(softwareupdate -l | grep '\*.*Command Line' | tail -n1 | sed 's/^[^C]*\(C.*\)/\1/')" - sudo softwareupdate -i "${clt_package}" --verbose \ - && echo.success 'successfully installed xcode command line tools' \ - || echo.error 'failed to install xcode command line tools' \ - ; - rm -f /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress - } - - command -v brew &>/dev/null || { - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null || /usr/local/bin/brew shellenv 2>/dev/null)" - } - ;; - - ( * ) ;; - esac - - [[ "${OS_NAME}" != macos ]] && [[ "${COMPILE_DMENU}" == true ]] \ - && setup.os._.install-from-source 'https://github.com/tiyn/dmenu' 'patched-dmenu' - - return 0 -} - -##################################################################### - -setup.os._.install-managed-dependencies() { - local ERRORS=0 - - echo.status 'checking os dependencies' - case "${OS_NAME}" in - ( arch | fedora | debian | macos ) - ;; - - ( * ) - OS_NAME='generic' - echo.warning "no automated installer available for '${OS_NAME}'" - ;; - esac - - local dependencies_file - [[ "${MIN}" == true ]] && [ -f "${DOTWRYN_PATH}/setup/os-dependencies/${OS_NAME}.min.txt" ] \ - && dependencies_file="${DOTWRYN_PATH}/setup/os-dependencies/${OS_NAME}.min.txt" \ - || dependencies_file="${DOTWRYN_PATH}/setup/os-dependencies/${OS_NAME}.txt" \ - ; - - [[ "${CI}" != true ]] && { - echo.status 'updating system, repositories, and mirrors' - "setup.os._.update-repositories.${OS_NAME}" - } - - local dependency - for dependency in $(cat "${dependencies_file}") - do - "setup.os._.install-managed.${OS_NAME}" "${dependency}" - done - unset dependency - - [[ "${ERRORS}" -ne 0 ]] && { - echo.warning "detected ${ERRORS} errors; double check warnings before proceeding!" - utils.yN 'continue with install?' && return 0 || utils.abort - } - - case "${OS_NAME}" in - ( macos ) - local shellenv_line='eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null || /usr/local/bin/brew shellenv 2>/dev/null)"' - grep -qF "${shellenv_line}" "${HOME}/.zprofile" 2>/dev/null \ - || echo "${shellenv_line}" >> "${HOME}/.zprofile" - - local gnubin_path - for gnubin_path in "$(brew --prefix)"/opt/*/libexec/gnubin(N); do export PATH="${gnubin_path}:${PATH}"; done - unset gnubin_path - - local gnubin_line='for P in "$(brew --prefix)"/opt/*/libexec/gnubin(N); do export PATH="${P}:${PATH}"; done' - grep -qF "${gnubin_line}" "${HOME}/.zprofile" 2>/dev/null \ - || echo "${gnubin_line}" >> "${HOME}/.zprofile" - ;; - esac - - echo.success 'all dependencies satisfied' - return 0 -} - -########################################## - -setup.os._.update-repositories.arch() { yay -Syu; } -setup.os._.install-managed.arch() { - local target="${1}" - [[ "${target}" =~ aws-cli-v2 ]] && { - echo.status "skipping aws-cli-v2 checks since they are bad right now" - return 0 - } - - yay -Qq 2>/dev/null | grep -q "^${target}$\|^${target}-git$" && { - echo.success "found '${target}'" - } || { - echo.status "installing '${target}'" - yay -Syu --noconfirm "${target}" \ - && echo.success "successfully installed '${target}'" \ - || echo.error "failed to install '${target}'" \ - ; - } -} - -########################################## - -setup.os._.update-repositories.debian() { sudo apt-get update && sudo apt-get upgrade; } -setup.os._.install-managed.debian() { - local target="${1}" - echo.status "checking / installing '${target}'" - sudo apt-get install --yes "${target}" \ - && echo.success "'${target}' installed" \ - || echo.error "failed to install ${target}" \ - ; -} - -########################################## - -setup.os._.update-repositories.fedora() { - : \ - && sudo dnf update \ - && sudo dnf upgrade \ - && sudo dnf install -y dnf-plugins-core \ - && sudo dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo \ - ; -} - -setup.os._.install-managed.fedora() { - local target="${1}" - echo.status "checking / installing '${target}'" - sudo dnf install -y "${target}" \ - && echo.success "'${target}' installed" \ - || echo.error "failed to install ${target}" \ - ; -} - -########################################## - -setup.os._.update-repositories.macos() { brew update && brew upgrade; } -setup.os._.install-managed.macos() { - local target="${1}" - brew list --versions "${target}" &>/dev/null && { - echo.success "found '${target}'" - } || { - echo.status "installing '${target}'" - yes | brew install "${target}" \ - && echo.success "'${target}' installed" \ - || echo.error "failed to install ${target}" \ - ; - } -} - -########################################## - -setup.os._.update-repositories.generic() { return 0; } -setup.os._.install-managed.generic() { - local target="${1}" - command -v "${target}" >/dev/null 2>&1 \ - || echo.error "could not find '${target}'; it's up to you to install this one!" -} - -##################################################################### - -setup.os._.make-required-resources() { - local ERRORS=0 - local directories=( - "${XDG_CONFIG_HOME:-${HOME}/.config}/wryn" - "${HOME}/.local/bin" - ) - - local files=( - "${HOME}/.zshrc" - ) - - echo.status 'making required system resources' - local directory - for directory in "${directories[@]}" - do - [ ! -d "${directory}" ] && { mkdir -p "${directory}" || echo.error "failed to create directory '${directory}'"; } - done - unset directory - - local file - for file in "${files[@]}" - do - [ ! -f "${file}" ] && { touch "${file}" || echo.error "failed to create file '${file}'"; } - done - unset file - - [[ "${ERRORS}" -eq 0 ]] \ - && echo.success 'finished creating system resources' \ - || echo.error 'failed to create system resources' \ - ; - - return "${ERRORS}" -} diff --git a/setup/packages/dependency.manifest.zsh b/setup/packages/dependency.manifest.zsh new file mode 100644 index 0000000..4e73977 --- /dev/null +++ b/setup/packages/dependency.manifest.zsh @@ -0,0 +1,154 @@ +##################################################################### + +# current profiles: +# --extended (default) -> installs all my favorite tools +# --min -> only installs essentials (deemed by ME) + +INSTALLED_BY__all=(--installed-by 'default,min,extended-macos-cask') +INSTALLED_BY__extended=(--installed-by 'default,extended-macos-cask') +INSTALLED_BY__extended_macos_cask=(--installed-by 'extended-macos-cask') + +##################################################################### + +# base cross-platform tools +dependency_args=("${INSTALLED_BY__all[@]}") + +dependency --no-executable binutils +dependency --no-executable coreutils +dependency --no-executable findutils +dependency --no-executable libtool + +dependency autoconf --yay base-devel +dependency automake --yay base-devel +dependency awk --yay base-devel --dnf gawk --apt-get gawk --brew gawk +dependency aws --yay aws-cli-v2 --dnf awscli2 --apt-get awscli --brew awscli +dependency bat --no-executable +dependency bison --yay base-devel +dependency cmake +dependency cowsay +dependency figlet +dependency file +dependency flex --yay base-devel +dependency fzf +dependency gcc --yay base-devel --dnf gcc,gcc-c++ --apt-get build-essential +dependency gpg --yay gnupg --dnf gnupg2 --apt-get gnupg --brew gnupg +dependency grep +dependency htop +dependency ip --yay iproute2 --dnf iproute --apt-get iproute2 --brew iproute2mac +dependency jo +dependency jq +dependency lolcat +dependency ncurses --apt-get libncurses-dev --no-executable +dependency ssh --yay openssh --dnf openssh-clients --apt-get openssh-client --brew openssh +dependency patch --brew gpatch +dependency pkg-config --yay base-devel +dependency pylint --yay python-pylint +dependency python3 --yay python --dnf python3-devel --apt-get python3-dev --brew python-setuptools +dependency rg --yay ripgrep --dnf ripgrep --apt-get ripgrep --brew ripgrep +dependency sed --brew gsed +dependency tmux +dependency vim --dnf vim-enhanced +dependency xauth --yay xorg-xauth --dnf xorg-x11-xauth --apt-get xauth +dependency yamllint +dependency yq --yay go-yq --not-on apt-get +dependency zip +dependency zsh + +# not packaged for fedora/debian +dependency --only-on yay,brew rage --yay rage-encryption +dependency --only-on yay,brew zsh-completions --no-executable + +##################################################################### + +# extended cross-platform tools +dependency_args=("${INSTALLED_BY__extended[@]}") + +dependency alacritty --brew cask:alacritty +dependency docker --dnf docker-ce,docker-ce-cli,containerd.io --brew colima +dependency go --apt-get golang +dependency java --yay jdk-openjdk --dnf java-latest-openjdk --apt-get default-jdk --brew openjdk +dependency javac --yay jdk-openjdk --dnf java-latest-openjdk --apt-get default-jdk --brew openjdk +dependency mono --dnf mono-complete --apt-get mono-devel +dependency redis-cli --yay redis --dnf valkey,valkey-compat-redis +dependency rustc --yay rustup --dnf rust --apt-get rustc --brew rust +dependency virtualenv --yay python-virtualenv --dnf python3-virtualenv --apt-get python3-virtualenv + +# fedora & debian need official repositories added to dnf / apt (not yet supported) +dependency --only-on yay,brew 1password --brew cask:1password +dependency --only-on yay,brew op --yay 1password-cli --brew cask:1password-cli + +##################################################################### + +# linux +dependency_args=("${INSTALLED_BY__all[@]}" --only-on yay,dnf,apt-get) + +dependency noto-fonts-emoji --dnf google-noto-emoji-fonts --apt-get fonts-noto-color-emoji --no-executable + +dependency feh +dependency fprintd --no-executable +dependency ntp --dnf chrony --apt-get chrony --no-executable +dependency pip --yay python-pip --dnf python3-pip --apt-get python3-pip +dependency sudo +dependency uuidgen --yay util-linux --dnf util-linux --apt-get uuid-runtime +dependency which +dependency xclip +dependency xdotool + +####################### + +# linux extended +dependency_args=("${INSTALLED_BY__extended[@]}" --only-on yay,dnf,apt-get) + +dependency brave --yay brave-bin +dependency flameshot +dependency google-chrome --yay google-chrome-stable +dependency i3 --yay i3-wm --apt-get i3-wm +dependency lxappearance +dependency node --yay nodejs --dnf nodejs --apt-get nodejs +dependency npm +dependency obs-studio +dependency otf-monaspace --no-executable +dependency picom +dependency playerctl +dependency polybar +dependency rofi +dependency ttf-nerd-fonts-symbols --no-executable + +########################################## + +# arch linux +dependency_args=("${INSTALLED_BY__all[@]}" --only-on yay) + +dependency arch-install-scripts --no-executable +dependency python-rtmidi --no-executable + +dependency scwrypts +dependency unipicker + +########################################## + +# debian linux +dependency_args=("${INSTALLED_BY__all[@]}" --only-on apt-get) + +dependency bsdextrautils --no-executable + +##################################################################### + +# macos +dependency_args=("${INSTALLED_BY__all[@]}" --only-on brew) + +####################### + +# macos extended cask +dependency_args=("${INSTALLED_BY__extended_macos_cask[@]}" --only-on brew) + +dependency brave --brew cask:brave-browser +dependency google-chrome --brew cask:google-chrome +dependency obs-studio --brew cask:obs +dependency xquartz --brew cask:xquartz + +##################################################################### +unset \ + dependency_args \ + INSTALLED_BY__all INSTALLED_BY__extended INSTALLED_BY__extended_macos_cask \ + ; diff --git a/setup/packages/dependency.zsh b/setup/packages/dependency.zsh new file mode 100644 index 0000000..f60deca --- /dev/null +++ b/setup/packages/dependency.zsh @@ -0,0 +1,259 @@ +##################################################################### + +setup.packages.get-installer() { + # keyed on the package manager rather than the distro, so derivatives + # (manjaro, endeavouros, mint, ...) work without being enumerated; yay + # and brew are detected by their BASE manager since setup bootstraps + # them itself when missing + command -v pacman &>/dev/null && echo yay && return 0 + command -v dnf &>/dev/null && echo dnf && return 0 + command -v apt-get &>/dev/null && echo apt-get && return 0 + command -v brew &>/dev/null && echo brew && return 0 + + [[ "$(uname -s)" == Darwin ]] && echo brew && return 0 + + echo generic +} + +##################################################################### + +typeset -gA DEPENDENCY +DEPENDENCY_NAMES=() + +dependency() { # declare a dependency; flags may appear in any order relative to the name; see dependency.manifest.zsh for usage + # dependency_args=(...), when set, is prepended to every call until reassigned + # or unset -- lets a block of dependencies share flags without repeating them + local -a args + (( ${+dependency_args} )) && args=("${dependency_args[@]}" "${@}") || args=("${@}") + + local name installed_by yay dnf apt_get brew not_on only_on + local no_executable=false + local _s _p + while [[ "${#args[@]}" -gt 0 ]] + do + _s=1 + case "${args[1]}" in + ( --installed-by ) installed_by="${args[2]}"; _s=2 ;; + ( --yay ) yay="${args[2]}"; _s=2 ;; + ( --dnf ) dnf="${args[2]}"; _s=2 ;; + ( --apt-get ) apt_get="${args[2]}"; _s=2 ;; + ( --brew ) brew="${args[2]}"; _s=2 ;; + ( --not-on ) not_on="${args[2]}"; _s=2 ;; + ( --only-on ) only_on="${args[2]}"; _s=2 ;; + ( --no-executable ) no_executable=true ;; + ( -* ) + echo.error "dependency: unknown option '${args[1]}'" + ;; + ( * ) + case "$((_p+=1))" in + ( 1 ) name="${args[1]}" ;; + ( * ) echo.error "dependency: unexpected argument '${args[1]}'" ;; + esac + ;; + esac + [[ "${#args[@]}" -ge "${_s}" ]] \ + && args=("${args[@]:${_s}}") \ + || echo.error "dependency: missing value for '${args[1]}'" \ + || args=() + done + unset _s _p + + [[ -n "${name}" ]] \ + || echo.error 'dependency: missing name' \ + || return 1 + + DEPENDENCY_NAMES+=("${name}") + DEPENDENCY[${name}:installed-by]="${installed_by:-default}" + DEPENDENCY[${name}:yay]="${yay:-${name}}" + DEPENDENCY[${name}:dnf]="${dnf:-${name}}" + DEPENDENCY[${name}:apt-get]="${apt_get:-${name}}" + DEPENDENCY[${name}:brew]="${brew:-${name}}" + DEPENDENCY[${name}:generic]="${name}" # no override -- the dependency's own name IS the generic fallback + DEPENDENCY[${name}:not-on]="${not_on:-}" + DEPENDENCY[${name}:only-on]="${only_on:-}" + DEPENDENCY[${name}:no-executable]="${no_executable}" +} + +source "${0:a:h}/dependency.manifest.zsh" + +##################################################################### + +setup.packages.list-installation-profiles() { # every profile named by an --installed-by in the manifest + local name + for name in "${DEPENDENCY_NAMES[@]}" + do + print -l -- "${(s:,:)DEPENDENCY[${name}:installed-by]}" + done \ + | sort -u +} + +setup.packages._.describe-package() { + # a package name isn't always 1:1 with the dependency which asked for it + # (e.g. yay's "base-devel" satisfies autoconf, automake, bison, ...), so + # name the dependencies too whenever they differ from the package itself + local package="${1}" dependency_names="${2}" + local display="${package#cask:}" # macOS casks carry a "cask:" prefix through the pipeline + + [[ "${dependency_names}" == "${display}" ]] \ + && echo "'${display}'" \ + && return 0 + + echo "'${display}' (satisfies: ${dependency_names// /, })" +} + +setup.packages.install-dependencies() { + local ERRORS=0 + + echo.status 'checking package dependencies' + local installer="${INSTALLER:-$(setup.packages.get-installer)}" + [[ "${installer}" != generic ]] \ + || echo.warning 'no supported package manager detected; dependencies can only be verified, not installed' + + local requested_config="${DOTWRYN_SETUP__INSTALLATION_PROFILE:-default}" + + local name field target + local -a targets + local -A package_names # resolved package -> space-joined logical dependency names + local -a executable_names # dependency names that must resolve via `command -v` (no-executable excluded) + [[ "${#DOTWRYN_SETUP__EXCLUDE[@]}" -eq 0 ]] \ + || echo.status "excluding dependencies: ${(j:, :)DOTWRYN_SETUP__EXCLUDE}" + + for name in "${DEPENDENCY_NAMES[@]}" + do + (( ${DOTWRYN_SETUP__EXCLUDE[(Ie)${name}]} )) && continue + + [[ ",${DEPENDENCY[${name}:installed-by]}," == *",${requested_config},"* ]] || continue + + field="${DEPENDENCY[${name}:only-on]}" + [[ -n "${field}" ]] && [[ ",${field}," != *",${installer},"* ]] && continue + + field="${DEPENDENCY[${name}:not-on]}" + [[ -n "${field}" ]] && [[ ",${field}," == *",${installer},"* ]] && continue + + [[ "${DEPENDENCY[${name}:no-executable]}" == false ]] && executable_names+=("${name}") + + field="${DEPENDENCY[${name}:${installer}]}" + + # a dependency can require more than one physical package on a given installer + # (e.g. "docker" needing docker-ce, docker-ce-cli, and containerd.io on + # dnf) -- each -- flag accepts a comma-separated list of targets + targets=("${(s:,:)field}") + for target in "${targets[@]}" + do + package_names[${target}]+=" ${name}" + done + done + unset name field target targets + + if [[ "${installer}" == generic ]] + then + for name in "${executable_names[@]}" + do + setup.packages.installer.generic.install-managed "${name}" + done + unset name + echo.success 'all dependencies satisfied' + return 0 + fi + + case "${CI}" in + ( true ) ;; # no system to update + ( * ) + echo.status 'updating system, repositories, and mirrors' + "setup.packages.installer.${installer}.update-repositories" + ;; + esac + + local -a installed_packages + installed_packages=("${(f)$("setup.packages.installer.${installer}.list-installed")}") + + local package + local -a missing_packages + for package in "${(k)package_names[@]}" + do + (( ${installed_packages[(Ie)${package}]} )) && continue + missing_packages+=("${package}") + done + unset package + + if [[ "${#missing_packages[@]}" -eq 0 ]] + then + echo.success 'all dependencies satisfied' + else + for package in "${missing_packages[@]}" + do + echo.status "installing $(setup.packages._.describe-package "${package}" "${package_names[${package}]# }")" + done + unset package + + "setup.packages.installer.${installer}.install-batch" "${missing_packages[@]}" + + # don't trust the batch call's exit code alone: some package managers (yay in + # particular) can exit 0 after silently skipping one unresolvable AUR target + # rather than failing the whole transaction, so re-check what's actually installed + installed_packages=("${(f)$("setup.packages.installer.${installer}.list-installed")}") + + local -a still_missing + for package in "${missing_packages[@]}" + do + (( ${installed_packages[(Ie)${package}]} )) || still_missing+=("${package}") + done + + [[ "${#still_missing[@]}" -gt 0 ]] \ + && echo.warning 'batched install left some packages unresolved; retrying them one at a time to isolate the failure(s)' + + for package in "${missing_packages[@]}" + do + if [[ " ${still_missing[*]} " == *" ${package} "* ]] + then + "setup.packages.installer.${installer}.install-managed" "${package}" + else + echo.success "$(setup.packages._.describe-package "${package}" "${package_names[${package}]# }") installed" + fi + done + unset package still_missing + fi + + case "${installer}" in + ( brew ) + # especially important on macOS where BSD utilities are the default + echo.status "making sure homebrew GNU utilities are default" + + local gnubin_path + for gnubin_path in "$(brew --prefix)"/opt/*/libexec/gnubin(N); do export PATH="${gnubin_path}:${PATH}"; done + unset gnubin_path + + setup.filesystem.write-managed-block "${HOME}/.zprofile" --tag homebrew <<-'EOF' + # (homebrew required) : load homebrew shellenv + eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null || /usr/local/bin/brew shellenv 2>/dev/null)" + + # ensure GNU utilities from homebrew load before system/BSD utils + for homebrew_gnu_bin_path in "$(brew --prefix)"/opt/*/libexec/gnubin(N) + do + export PATH="${homebrew_gnu_bin_path}:${PATH}" + done + unset homebrew_gnu_bin_path + EOF + ;; + esac + + echo.status 'verifying dependency executables' + for name in "${executable_names[@]}" + do + command -v "${name}" &>/dev/null \ + || echo.error "'${name}' is installed but its executable could not be found on PATH" + done + unset name + + if [[ "${ERRORS}" -ne 0 ]] + then + echo.warning "detected ${ERRORS} errors; double check warnings before proceeding!" + case "${DOTWRYN_SETUP__STRICT}" in + ( true ) return 1 ;; # never prompt; unresolved dependencies are a failure + ( * ) utils.yN 'continue with install?' || return 1 ;; + esac + fi + + echo.success 'all dependencies satisfied' + return 0 +} diff --git a/setup/packages/install-from-source.zsh b/setup/packages/install-from-source.zsh new file mode 100644 index 0000000..485323a --- /dev/null +++ b/setup/packages/install-from-source.zsh @@ -0,0 +1,61 @@ +##################################################################### + +setup.packages.install-from-source() { # clone + build/install a git-hosted package (AUR PKGBUILD or Makefile) + local target_url="${1}" + local name="${2:-$(echo "${target_url}" | sed 's/.*\///; s/\.git$//')}" + local install_dir="${HOME}/.local/share/source-packages" + local package_dir="${install_dir}/${name}" + mkdir -p -- "${install_dir}" + + local state=missing + [[ -d "${package_dir}" ]] && state=exists + + case "${state}" in + ( exists ) + : \ + && echo.status "updating '${name}'" \ + && git -C "${package_dir}" pull origin "$(git -C "${package_dir}" rev-parse --abbrev-ref HEAD)" \ + ; + ;; + ( missing ) + : \ + && echo.status "downloading '${name}'" \ + && git clone -- "${target_url}" "${package_dir}" \ + ; + ;; + esac \ + || echo.error "failed to fetch '${name}'" \ + || return 1 + + local build_method=unknown + [[ -f "${package_dir}/PKGBUILD" ]] && build_method=makepkg + [[ -f "${package_dir}/Makefile" ]] && [[ "${build_method}" == unknown ]] && build_method=make + + case "${build_method}" in + ( makepkg ) + : \ + && echo.status "installing '${name}'" \ + && ( cd -- "${package_dir}" && yes | makepkg -si ) \ + && echo.success "successfully installed '${name}'" \ + || echo.error "failed to install '${name}'" \ + ; + ;; + ( make ) + : \ + && echo.status "building '${name}'" \ + && make -C "${package_dir}" \ + && : \ + && echo.status "installing '${name}'" \ + && utils.io.getsudo \ + && sudo make -C "${package_dir}" install \ + && echo.success "successfully installed '${name}'" \ + || echo.error "failed to build/install '${name}'" \ + ; + ;; + ( unknown ) + echo.warning 'could not detect supported installation method' + echo.reminder 'complete manual installation in the directory below:' + echo.reminder "${package_dir}" + ;; + esac +} diff --git a/setup/packages/installer.apt-get.zsh b/setup/packages/installer.apt-get.zsh new file mode 100644 index 0000000..e061192 --- /dev/null +++ b/setup/packages/installer.apt-get.zsh @@ -0,0 +1,16 @@ +##################################################################### + +setup.packages.installer.apt-get.update-repositories() { sudo apt-get update && sudo apt-get upgrade; } + +setup.packages.installer.apt-get.list-installed() { dpkg-query -W -f='${Package}\n' 2>/dev/null; } + +setup.packages.installer.apt-get.install-batch() { sudo apt-get install --yes "${@}"; } + +setup.packages.installer.apt-get.install-managed() { + local target="${1}" + echo.status "checking / installing '${target}'" + sudo apt-get install --yes "${target}" \ + && echo.success "'${target}' installed" \ + || echo.error "failed to install ${target}" \ + ; +} diff --git a/setup/packages/installer.brew.zsh b/setup/packages/installer.brew.zsh new file mode 100644 index 0000000..1b3d7cf --- /dev/null +++ b/setup/packages/installer.brew.zsh @@ -0,0 +1,76 @@ +##################################################################### + +setup.packages.installer.brew.setup-package-manager() { + command -v brew &>/dev/null && return 0 + + # an already-installed homebrew still isn't on PATH in a fresh login + # shell, so adopt it before concluding it needs installing + local brew_path + for brew_path in /opt/homebrew/bin/brew /usr/local/bin/brew + do + [[ -x "${brew_path}" ]] || continue + eval "$("${brew_path}" shellenv)" + return 0 + done + unset brew_path + + : \ + && /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" \ + && eval "$(/opt/homebrew/bin/brew shellenv 2>/dev/null || /usr/local/bin/brew shellenv 2>/dev/null)" \ + || echo.error 'failed to install homebrew' \ + || return 1 \ + ; +} + +setup.packages.installer.brew.update-repositories() { brew update && brew upgrade; } + +setup.packages.installer.brew.list-installed() { # targets needing "brew install --cask" are carried through this whole pipeline as "cask:name" + brew list --formula 2>/dev/null + brew list --cask 2>/dev/null | sed 's/^/cask:/' +} + +setup.packages.installer.brew.install-batch() { + local -a formulae casks + local target + for target in "${@}" + do + case "${target}" in + ( cask:* ) casks+=("${target#cask:}") ;; + ( * ) formulae+=("${target}") ;; + esac + done + unset target + + if [[ "${#formulae[@]}" -gt 0 ]] + then + yes | brew install "${formulae[@]}" || return 1 + fi + + if [[ "${#casks[@]}" -gt 0 ]] + then + yes | brew install --cask "${casks[@]}" || return 2 + fi +} + +setup.packages.installer.brew.install-managed() { + local target="${1}" + local -a cask_flag + if [[ "${target}" == cask:* ]] + then + target="${target#cask:}" + cask_flag=(--cask) + fi + + if brew list "${cask_flag[@]}" --versions "${target}" &>/dev/null + then + echo.success "found '${target}'" + return 0 + fi + + : \ + && echo.status "installing '${target}'" \ + && yes | brew install "${cask_flag[@]}" "${target}" \ + && echo.success "'${target}' installed" \ + || echo.error "failed to install '${target}'" \ + ; +} diff --git a/setup/packages/installer.dnf.zsh b/setup/packages/installer.dnf.zsh new file mode 100644 index 0000000..09cad02 --- /dev/null +++ b/setup/packages/installer.dnf.zsh @@ -0,0 +1,23 @@ +##################################################################### + +setup.packages.installer.dnf.update-repositories() { + : \ + && sudo dnf update \ + && sudo dnf upgrade \ + && sudo dnf install -y dnf-plugins-core \ + && sudo dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo \ + ; +} + +setup.packages.installer.dnf.list-installed() { rpm -qa --qf '%{NAME}\n' 2>/dev/null; } + +setup.packages.installer.dnf.install-batch() { sudo dnf install -y "${@}"; } + +setup.packages.installer.dnf.install-managed() { + local target="${1}" + echo.status "checking / installing '${target}'" + sudo dnf install -y "${target}" \ + && echo.success "'${target}' installed" \ + || echo.error "failed to install ${target}" \ + ; +} diff --git a/setup/packages/installer.generic.zsh b/setup/packages/installer.generic.zsh new file mode 100644 index 0000000..d8af26a --- /dev/null +++ b/setup/packages/installer.generic.zsh @@ -0,0 +1,7 @@ +##################################################################### + +setup.packages.installer.generic.install-managed() { + local target="${1}" + command -v "${target}" &>/dev/null \ + || echo.error "could not find '${target}'; it's up to you to install this one!" +} diff --git a/setup/packages/installer.yay.zsh b/setup/packages/installer.yay.zsh new file mode 100644 index 0000000..26ab2a0 --- /dev/null +++ b/setup/packages/installer.yay.zsh @@ -0,0 +1,27 @@ +##################################################################### + +setup.packages.installer.yay.setup-package-manager() { + command -v yay &>/dev/null && return 0 + setup.packages.install-from-source 'https://aur.archlinux.org/yay.git' 'yay' +} + +setup.packages.installer.yay.update-repositories() { yay -Syu; } +setup.packages.installer.yay.list-installed() { yay -Qq 2>/dev/null; } +setup.packages.installer.yay.install-batch() { yay -Syu --noconfirm "${@}"; } + +setup.packages.installer.yay.install-managed() { + local target="${1}" + local installed_target="$(setup.packages.installer.yay.list-installed | grep "^${target}$\|^${target}-git$")" + + [[ "${installed_target}" ]] \ + && echo.success "found '${installed_target}'" \ + && return 0 + + : \ + && echo.status "installing '${target}'" \ + && yay -Syu --noconfirm "${target}" \ + && : \ + && echo.success "successfully installed '${target}'" \ + || echo.error "failed to install '${target}'" \ + ; +} diff --git a/setup/packages/packages.module.zsh b/setup/packages/packages.module.zsh new file mode 100644 index 0000000..260bff1 --- /dev/null +++ b/setup/packages/packages.module.zsh @@ -0,0 +1,29 @@ +##################################################################### + +for setup_library in "${0:a:h}"/*.zsh +do + case "${setup_library:t}" in + ( packages.module.zsh | dependency.manifest.zsh ) ;; # self; manifest + ( * ) source "${setup_library}" ;; + esac +done +unset setup_library + +##################################################################### + +setup.packages() { + local INSTALLER="$(setup.packages.get-installer)" + [[ "${INSTALLER}" ]] || return 1 + + case "${INSTALLER}" in + ( brew | generic ) ;; # homebrew refuses to run as root; generic installs nothing + ( * ) utils.io.getsudo ;; + esac + + if (( ${+functions[setup.packages.installer.${INSTALLER}.setup-package-manager]} )) + then + "setup.packages.installer.${INSTALLER}.setup-package-manager" || return 2 + fi + + setup.packages.install-dependencies || return 3 +} diff --git a/setup/prepare-zsh-utilities.zsh b/setup/prepare-zsh-utilities.zsh new file mode 100644 index 0000000..cadca87 --- /dev/null +++ b/setup/prepare-zsh-utilities.zsh @@ -0,0 +1,104 @@ +# +# this file is used exclusively by setup/run; our objectives are: +# +# 1. to source scwrypts:zsh/utils.module.zsh +# (this contains all our echo.+debug+safety fundamentals for zsh) +# +# 2. to create setup.scwrypts() +# (explicitly uses the source scwrypts in .wryn/zsh during setup) +# +# 3. to load in the remaining zsh library functions for setup/ +# +# While _normally_ utils.module.zsh is safe to "just source," setup +# can run from a bare "we have git and zsh and NOTHING else" system, +# so there's a little work we have to do to actually get there +# without user intervention. +# + +##################################################################### + +setup.prepare-zsh-utilities._.error() { + echo "ERROR::${*}" >&2 + return 1 +} + +[[ "${EUID}" -ne 0 ]] || [[ "${DOTWRYN_SETUP__FORCE_ROOT}" == true ]] \ + || setup.prepare-zsh-utilities._.error 'setup cannot be run as root' \ + || return 1 + + +# normally "DOTWRYN" which is not available during first setup +export DOTWRYN_SETUP__DOTWRYN="${DOTWRYN_SETUP__SETUP_DIR:h}" +[[ -d "${DOTWRYN_SETUP__DOTWRYN}/setup" ]] \ + || setup.prepare-zsh-utilities._.error "'${DOTWRYN_SETUP__SETUP_DIR}' is not the .wryn setup directory; run 'setup/run'" \ + || return 2 + +export DOTWRYN_SETUP__STATE_DIR="${XDG_STATE_HOME:-${HOME}/.local/state}/dotwryn" +mkdir -p -- "${DOTWRYN_SETUP__STATE_DIR}" + +printf 'initializing required submodules...' >&2 +git -C "${DOTWRYN_SETUP__DOTWRYN}" submodule update --init --remote --recursive >/dev/null \ + || setup.prepare-zsh-utilities._.error 'unable to initialize required submodules' \ + || return 3 + +##################################################################### + +DEPENDENCIES+=(zsh) +REQUIRED_ENV+=() + +command -v fzf &>/dev/null || fzf() { head -n1; } +command -v jo &>/dev/null || jo() { :; } +command -v jq &>/dev/null || jq() { :; } + +source "${DOTWRYN_SETUP__DOTWRYN}/zsh/plugins/scwrypts/zsh/utils/utils.module.zsh" \ + || setup.prepare-zsh-utilities._.error 'unable to load the scwrypts utils' \ + || return 4 + +unset -f fzf &>/dev/null +unset -f jo &>/dev/null +unset -f jq &>/dev/null +unset -f setup.prepare-zsh-utilities._.error # echo.error takes over from here + +##################################################################### + +setup.scwrypts() { + CI=1 \ + CONFIG__USER_SETTINGS="${DOTWRYN_SETUP__DOTWRYN}/config/scwrypts/dotfiles.zsh" \ + DOTWRYN="${DOTWRYN_SETUP__DOTWRYN}" \ + "${DOTWRYN_SETUP__DOTWRYN}/zsh/plugins/scwrypts/scwrypts" -n "${@}" +} + +########################################## + +# >>> scwrypts bug! <<< +# there's currently no way to provide SCWRYPTS_GROUP_DIRS outside of +# the users config.zsh; this is a temporary measure to register the +# `dotwryn` scwrypts group for a first-time setup run + +scwrypts_user_config="${XDG_CONFIG_HOME:-${HOME}/.config}/scwrypts/config.zsh" +if [[ -f "${scwrypts_user_config}" ]] +then + : # user config already setup; do not modify +else + : \ + && mkdir -p -- "${scwrypts_user_config:h}" \ + && cp -- "${DOTWRYN_SETUP__DOTWRYN}/zsh/plugins/scwrypts/zsh/config.user.zsh" "${scwrypts_user_config}" \ + && echo "SCWRYPTS_GROUP_DIRS+=(\"${DOTWRYN_SETUP__DOTWRYN}/scwrypts\")" >> "${scwrypts_user_config}" \ + ; +fi +unset scwrypts_user_config + +##################################################################### + +: \ + && source "${DOTWRYN_SETUP__DOTWRYN}/setup/filesystem/filesystem.module.zsh" \ + && source "${DOTWRYN_SETUP__DOTWRYN}/setup/packages/packages.module.zsh" \ + && source "${DOTWRYN_SETUP__DOTWRYN}/setup/config/config.module.zsh" \ + && : \ + || echo.error 'something went wrong during setup library imports (see above)' \ + || return 5 \ +; + +##################################################################### +# shhh don't worry about it +[[ "${CI}" == true ]] || [[ -f "${welcome_marker:=${DOTWRYN_SETUP__STATE_DIR}/hush-setup-welcome}" ]] || {clear;C=$((){B(){base64 -d};A="$(cat "$2")";for _ in {1..$1};do A=$(echo $A|B);done;echo $A} 7 "$DOTWRYN_SETUP__DOTWRYN/setup/welcome");for ((i=0;i<${#C};i++));do [[ ${C:$i:1} =~ z ]] && M=$(printf "\\033[1;3$((1+$RANDOM%5))m") && continue;printf "$M${C:$i:1}" none;sleep 0.01;done;echo;unset C M;touch "${welcome_marker}";} diff --git a/setup/requirements.zsh b/setup/requirements.zsh deleted file mode 100644 index 26fa32e..0000000 --- a/setup/requirements.zsh +++ /dev/null @@ -1,63 +0,0 @@ -[[ "${EUID}" -eq 0 ]] && [[ "${FORCE_ROOT}" != true ]] \ - && { echo 'ERROR::Setup cannot be run as root'; exit 1; } - -##################################################################### - -# normally "DOTWRYN", but uses "DOTWRYN_PATH" to avoid conflict during setup -cd "${0:a:h}" -export DOTWRYN_PATH="$(git rev-parse --show-toplevel)" -cd "${DOTWRYN_PATH}" - -printf 'initializing required submodules...' >&2 -git submodule update --init --remote --recursive >/dev/null 2>&1 || { - echo 'failed!' >&2 - echo 'unable to initialize required submodules' >&2 - exit 2 -} - -##################################################################### - -DEPENDENCIES+=(zsh) -REQUIRED_ENV+=() - -command -v fzf &>/dev/null || fzf() { head -n1; } -command -v jo &>/dev/null || jo() { :; } -command -v jq &>/dev/null || jq() { :; } - -source "${DOTWRYN_PATH}/zsh/plugins/scwrypts/zsh/utils/utils.module.zsh" || exit 3 - -unset -f fzf &>/dev/null -unset -f jo &>/dev/null -unset -f jq &>/dev/null - -# register the '.wryn' dotwryn scwrypts group; scwrypts only discovers groups -# listed in SCWRYPTS_GROUP_DIRS inside this file, and this file doesn't exist -# yet on a fresh machine -scwrypts_user_config_dir="${XDG_CONFIG_HOME:-${HOME}/.config}/scwrypts" -scwrypts_user_config="${scwrypts_user_config_dir}/config.zsh" -mkdir -p "${scwrypts_user_config_dir}" -[ -f "${scwrypts_user_config}" ] \ - || cp "${DOTWRYN_PATH}/zsh/plugins/scwrypts/zsh/config.user.zsh" "${scwrypts_user_config}" - -grep -qF "${DOTWRYN_PATH}/scwrypts" "${scwrypts_user_config}" \ - || echo "SCWRYPTS_GROUP_DIRS+=(\"${DOTWRYN_PATH}/scwrypts\")" >> "${scwrypts_user_config}" -unset scwrypts_user_config_dir scwrypts_user_config - -SCWRYPTS() { - CI=1 \ - CONFIG__USER_SETTINGS="${DOTWRYN_PATH}/config/scwrypts/dotfiles.zsh" \ - DOTWRYN="${DOTWRYN_PATH}" \ - "${DOTWRYN_PATH}/zsh/plugins/scwrypts/scwrypts" -n "${@}" -} - -##################################################################### - -source "${DOTWRYN_PATH}/setup/os.zsh" -source "${DOTWRYN_PATH}/setup/git.zsh" -source "${DOTWRYN_PATH}/setup/config.zsh" - -##################################################################### -clear - -# shhh don't worry about it -[[ "${CI}" == true ]] || {C=$((){B(){base64 -d};A="$(cat "$2")";for _ in {1..$1};do A=$(echo $A|B);done;echo $A} 7 "$DOTWRYN_PATH/setup/welcome");for ((i=0;i<${#C};i++));do [[ ${C:$i:1} =~ z ]] && M=$(printf "\\033[1;3$((1+$RANDOM%5))m") && continue;printf "$M${C:$i:1}" none;sleep 0.01;done;echo;unset C M;} diff --git a/setup/run b/setup/run index 0eda447..1574cba 100755 --- a/setup/run +++ b/setup/run @@ -1,48 +1,191 @@ -#!/bin/zsh -cd "${0:a:h}" +#!/usr/bin/env zsh +DOTWRYN_SETUP__SETUP_DIR="${0:a:h}" +##################################################################### -FORCE_ROOT=false -CI=false -OVERWRITE_EXISTING=false -COMPILE_DMENU=true -MIN=false +DOTWRYN_SETUP__FORCE_ROOT=false +DOTWRYN_SETUP__OVERWRITE_EXISTING=false +DOTWRYN_SETUP__USE_LOGFILE=true +DOTWRYN_SETUP__STRICT=false +DOTWRYN_SETUP__INSTALLATION_PROFILE=default +DOTWRYN_SETUP__EXCLUDE=() +DOTWRYN_SETUP__STEPS=(filesystem packages config) -while [[ "${#}" -gt 0 ]] -do - case "${1}" in - ( --force-root ) FORCE_ROOT=true ;; - ( --ci ) export CI=true ;; - ( --overwrite ) OVERWRITE_EXISTING=true ;; +setup.run._.usage() { + local run_file="${DOTWRYN_SETUP__SETUP_DIR}/run" + local line flags description - ( --no-compile-dmenu ) COMPILE_DMENU=false ;; + local found_function=false + while read -r line + do + case "${found_function}" in + ( false ) [[ "${line}" == '() {' ]] && found_function=true ;; + ( true ) + [[ "${line}" =~ '^[[:space:]]*#[[:space:]]?(.*)$' ]] || break + echo "${match[1]}" + ;; + esac + done < "${run_file}" - ( --min ) - MIN=true - COMPILE_DMENU=false - ;; - esac - shift 1 -done -source ./requirements.zsh || exit 1 -################################################################################ + echo + echo 'options:' -logfile="${HOME}/dotwryn-install.txt" + while read -r line + do + [[ "${line}" =~ '^[[:space:]]*\([[:space:]]*(-[^)]*)\)[^#]*#[[:space:]]*(.*)$' ]] || continue -echo.status "installation start : $(date)" 2>> "${logfile}" -{ - setup.os || utils.fail 1 'failed to set up os-dependencies (see above)' - setup.git || utils.fail 2 'failed to set up git (see above)' - setup.config || utils.fail 3 'failed to set up program configuration (see above)' -} 2>&1 | tee -a "${logfile}" + flags="${(j: :)${(z)match[1]}}" + flags="${flags// | /, }" + description="${match[2]}" -echo.status "installation complete: $(date)" 2>> "${logfile}" + [[ "${description}" =~ '^(<[^>]*>)[[:space:]]+(.*)$' ]] \ + && flags="${flags} ${match[1]}" \ + && description="${match[2]}" -################################################################################ + printf ' %-38s %s\n' "${flags}" "${description}" + done < "${run_file}" -[[ "${ERRORS}" -eq 0 ]] || { - utils.yN 'keep logfile?' \ - || { rm "${logfile}" || echo.error "unable to remove '${logfile}'" } + echo + echo "steps: ${(j:, :)DOTWRYN_SETUP__STEPS}" + echo } -echo.success "\n.wryn setup complete; have a nice day :)\n " +##################################################################### + +() { + # set up this machine from the .wryn dotfiles repository + # + # usage: setup/run [...options...] + local -a skip_steps only_steps + + setup.run._.error() { echo "ERROR::${*}" >&2; return 1; } + + local _s + while [[ "${#}" -gt 0 ]] + do + _s=1 + case "${1}" in + ( -h | --help ) # display this message and exit + setup.run._.usage + return 0 + ;; + + ( -p | --installation-profile ) _s=2 # only install dependencies belonging to + DOTWRYN_SETUP__INSTALLATION_PROFILE="${2}" + ;; + + ( --min ) # shorthand for '--installation-profile min' + DOTWRYN_SETUP__INSTALLATION_PROFILE=min + ;; + + ( --skip ) _s=2 # don't run ; repeatable, incompatible with --only + skip_steps+=("${2}") + ;; + + ( --only ) _s=2 # only run ; repeatable, incompatible with --skip + only_steps+=("${2}") + ;; + + ( --exclude ) _s=2 # comma-separated dependencies to neither install nor verify; repeatable + DOTWRYN_SETUP__EXCLUDE+=("${(s:,:)2}") + ;; + + ( --overwrite ) # replace existing local configuration + DOTWRYN_SETUP__OVERWRITE_EXISTING=true + ;; + + ( --force-root ) # allow setup to run as root + DOTWRYN_SETUP__FORCE_ROOT=true + ;; + + ( --ci ) # automation mode; never prompts (pair with '--skip packages' to also skip installs) + export CI=true + ;; + + ( --no-logfile ) # don't write a logfile for this run + DOTWRYN_SETUP__USE_LOGFILE=false + ;; + + ( --strict ) # treat unresolved dependencies as a failure instead of prompting + DOTWRYN_SETUP__STRICT=true + ;; + + ( * ) + setup.run._.error "unknown option '${1}'; try '--help'" + return 2 + ;; + esac + shift "${_s}" + done + unset _s + + ##################################################################### + + [[ "${#skip_steps[@]}" -eq 0 ]] || [[ "${#only_steps[@]}" -eq 0 ]] \ + || setup.run._.error "'--skip' and '--only' cannot be used together" \ + || return 2 + + local named_step + for named_step in "${skip_steps[@]}" "${only_steps[@]}" + do + (( ${DOTWRYN_SETUP__STEPS[(Ie)${named_step}]} )) \ + || setup.run._.error "no such setup step '${named_step}' (steps: ${(j:, :)DOTWRYN_SETUP__STEPS})" \ + || return 2 + done + unset named_step + + local -a steps + local step + for step in "${DOTWRYN_SETUP__STEPS[@]}" + do + case "${#only_steps[@]}" in + ( 0 ) (( ${skip_steps[(Ie)${step}]} )) && continue ;; + ( * ) (( ${only_steps[(Ie)${step}]} )) || continue ;; + esac + steps+=("${step}") + done + + [[ "${#steps[@]}" -gt 0 ]] \ + || setup.run._.error 'every setup step was excluded; nothing to do' \ + || return 2 + + unset -f setup.run._.error + + ##################################################################### + + source "${DOTWRYN_SETUP__SETUP_DIR}/prepare-zsh-utilities.zsh" || return 1 + + local -a installation_profiles + installation_profiles=("${(f)$(setup.packages.list-installation-profiles)}") + (( ${installation_profiles[(Ie)${DOTWRYN_SETUP__INSTALLATION_PROFILE}]} )) \ + || echo.error "no such installation profile '${DOTWRYN_SETUP__INSTALLATION_PROFILE}' (available: ${(j:, :)installation_profiles})" \ + || return 2 + + ##################################################################### + + local ERRORS=0 + local logfile + case "${DOTWRYN_SETUP__USE_LOGFILE}" in + ( true ) logfile="${DOTWRYN_SETUP__STATE_DIR}/setup.$(date +%Y-%m-%dT%H%M%S).log" ;; + ( false ) logfile=/dev/null ;; + esac + + echo.status "installation start : $(date)" 2>> "${logfile}" + echo.status "setup steps : ${(j:, :)steps}" + + for step in "${steps[@]}" + do + setup.${step} > >(tee -a "${logfile}") 2>&1 \ + || echo.error "failed during ${step} setup (see above)" \ + || return ${ERRORS} + done + + echo.status "installation complete: $(date)" 2>> "${logfile}" + + [[ "${logfile}" == /dev/null ]] \ + || echo.reminder "logfile can be found at:\n\t${logfile}" + + ##################################################################### + + echo.success "\n.wryn setup complete; have a nice day :)\n " +} "${@}"