├── .gitignore ├── .goreleaser.yaml ├── README.md ├── embed ├── elan-init.ps1 ├── elan-init.sh └── init.go ├── glean ├── config.go ├── elan.go ├── lake.go ├── lean.go └── update.go ├── go.mod ├── go.sum └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | 3 | dist/ 4 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | 4 | # The lines bellow are called `modelines`. See `:help modeline` 5 | # Feel free to remove those if you don't want/need to use them. 6 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 7 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 8 | 9 | before: 10 | hooks: 11 | # You may remove this if you don't use go modules. 12 | - go mod tidy 13 | # you may remove this if you don't need go generate 14 | - go generate ./... 15 | 16 | builds: 17 | - env: 18 | - CGO_ENABLED=0 19 | goos: 20 | - linux 21 | - windows 22 | - darwin 23 | goarch: 24 | - amd64 25 | - arm64 26 | 27 | archives: 28 | - format: tar.gz 29 | # this name template makes the OS and Arch compatible with the results of `uname`. 30 | name_template: >- 31 | {{ .ProjectName }}_ 32 | {{- title .Os }}_ 33 | {{- if eq .Arch "amd64" }}x86_64 34 | {{- else if eq .Arch "386" }}i386 35 | {{- else }}{{ .Arch }}{{ end }} 36 | {{- if .Arm }}v{{ .Arm }}{{ end }} 37 | # use zip for windows archives 38 | format_overrides: 39 | - goos: windows 40 | format: zip 41 | 42 | changelog: 43 | sort: asc 44 | filters: 45 | exclude: 46 | - "^docs:" 47 | - "^test:" 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `glean`: Lean 4 镜像适配工具 2 | 3 | 使用上海交通大学的 https://mirror.sjtu.edu.cn 镜像服务,软件源镜像托管在 `elan` 4 | 和 `git/lean4-packages` 下。 5 | 6 | 请自行修改命令中的版本号,可用版本参见: 7 | http://mirror.sjtu.edu.cn/elan/?mirror_intel_list 8 | 9 | 也可以通过这个链接下载 glean。 10 | 11 | ## 安装 Elan 12 | 13 | Elan 是 Lean 的版本管理工具,在 Lake 调用时根据项目 `lean-toolchain` 文件下载安装 Lean 并切换到对应的版本。 14 | 15 | 例如要阅读 Mathematics in Lean,可以运行 16 | 17 | ```sh 18 | git clone --depth 1 https://mirror.sjtu.edu.cn/git/lean4-packages/mathematics_in_lean/ 19 | ``` 20 | 21 | 然后通过 `cat lean-toolchain` 获取需要安装的版本。 22 | 23 | ```sh 24 | glean -install elan -version 3.1.1 25 | ``` 26 | 27 | ## 安装 Lean 28 | 29 | 以下操作会安装 Lean 与 Lean 工具链,包含语言服务器、构建工具等。 30 | 31 | ```sh 32 | glean -install lean --version 4.5.0 33 | ``` 34 | 35 | 如需安装 nightly 版本,请以如下例子中的格式编辑命令。 36 | 37 | ```sh 38 | glean -install lean --version 4.4.0-nightly-2023-11-12 39 | ``` 40 | 41 | nightly 可用版本参见 [lean4_nightly](http://mirror.sjtu.edu.cn/elan/leanprover/lean4_nightly/releases/download?mirror_intel_list) 42 | 43 | ## 在构建项目前下载依赖 44 | 45 | 每当下载完一个 Lean 项目后,在启动 VSCode 或命令行运行 `lake build` 前,可以提前通过镜像下载依赖。 46 | 47 | ```sh 48 | glean -lake-manifest-path ~/EG/lake-manifest.json 49 | ``` 50 | 51 | 此处使用选项 `-lake-manifest-path ~/EG/lake-manifest.json` 来手动指定了 `lake-manifest.json` 文件的位置。 52 | 也可以在进入一个 Lean4 Lake 项目后,直接运行 53 | 54 | ```sh 55 | glean 56 | ``` 57 | 58 | 命令。这样,glean 会自动找到当前项目中 `lake-manifest.json` 文件的位置。 59 | 60 | ### 如何判断自己是否在在一个 Lean4 Lake 项目路径工作? 61 | 62 | 运行 `ls` 命令,如果能找到 `lakefile.lean`,即代表正在 Lean4 Lake 项目路径工作。 63 | 64 | 使用 `code .` 启动 VSCode 来使用 Lean 时,也需要确保正在 Lean4 Lake 项目路径工作。 65 | -------------------------------------------------------------------------------- /embed/elan-init.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | . 4 | 5 | .DESCRIPTION 6 | This is just a little script that can be downloaded from the Internet to 7 | install elan. It just does platform detection, downloads the latest 8 | installer, then runs it. 9 | 10 | .PARAMETER Verbose 11 | Produce verbose output about the elan installation process. 12 | 13 | .PARAMETER NoPrompt 14 | Do not present elan installation menu of choices. 15 | 16 | .PARAMETER NoModifyPath 17 | Do not modify PATH environment variable. 18 | 19 | .PARAMETER DefaultToolchain 20 | Which tool chain to setup as your default toolchain, or specify 'none' 21 | 22 | .PARAMETER ElanRoot 23 | Where to find the elan-init tool, default is https://github.com/leanprover/elan/releases 24 | 25 | .PARAMETER ElanVersion 26 | Specific version of elan to download and run instead of latest, e.g. 1.4.1 27 | #> 28 | param( 29 | [bool] $Verbose = 0, 30 | [bool] $NoPrompt = 1, 31 | [bool] $NoModifyPath = 0, 32 | [string] $DefaultToolchain = "none", 33 | [string] $ElanRoot = "https://mirror.sjtu.edu.cn/elan/elan/releases", 34 | # [string] $ElanVersion = "v3.1.1" 35 | [string] $ElanVersion = "__INPUT_VERSION__" 36 | ) 37 | 38 | # There are no Windows ARM releases yet, use emulated x64 39 | $_arch = "x86_64-pc-windows-msvc" 40 | $_ext = ".exe" 41 | $temp = [System.IO.Path]::GetTempPath() 42 | $_dir = Join-Path $temp "elan" 43 | if (-not (Test-Path -Path $_dir)) { 44 | $null = New-Item -ItemType Directory -Path $_dir 45 | } 46 | $_file = "$_dir/elan-init$_ext" 47 | 48 | Write-Host "info: downloading installer to ${temp}" 49 | 50 | try { 51 | [string] $DownloadUrl = "" 52 | if ($ElanVersion.Length -gt 0) { 53 | $DownloadUrl = "$ElanRoot/download/$ElanVersion/elan-$_arch.zip" 54 | } 55 | else { 56 | $DownloadUrl = "$ElanRoot/latest/download/elan-$_arch.zip" 57 | } 58 | $null = Start-BitsTransfer -Source $DownloadUrl -Destination "$_dir/elan-init.zip" -ErrorAction Stop 59 | } 60 | catch { 61 | Write-Host "Download failed for ${DownloadUrl}" 62 | return 1 63 | } 64 | 65 | $null = Expand-Archive -Path "$_dir/elan-init.zip" -DestinationPath "$_dir" -Force 66 | 67 | $cmdline = " " 68 | if ($DefaultToolchain -ne "") { 69 | $cmdline += "--default-toolchain $DefaultToolchain" 70 | } 71 | if ($NoPrompt) { 72 | $cmdline += " -y" 73 | } 74 | if ($NoModifyPath) { 75 | $cmdline += " --no-modify-path" 76 | } 77 | if ($Verbose) { 78 | $cmdline += " --verbose" 79 | } 80 | $details = Start-Process -FilePath "$_file" -ArgumentList $cmdline -Wait -NoNewWindow -Passthru 81 | 82 | $rc = $details.exitCode 83 | if ($rc -ne 0 ) { 84 | Write-Host "Elan failed with error code $rc" 85 | return 1 86 | } 87 | 88 | $null = Remove-Item -Recurse -Force "$_dir" 89 | 90 | return 0 91 | -------------------------------------------------------------------------------- /embed/elan-init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright 2016 The Rust Project Developers. See the COPYRIGHT 3 | # file at the top-level directory of this distribution and at 4 | # http://rust-lang.org/COPYRIGHT. 5 | # 6 | # Licensed under the Apache License, Version 2.0 or the MIT license 8 | # , at your 9 | # option. This file may not be copied, modified, or distributed 10 | # except according to those terms. 11 | 12 | # This is just a little script that can be downloaded from the internet to 13 | # install elan. It just does platform detection, downloads the installer 14 | # and runs it. 15 | 16 | set -u 17 | 18 | ELAN_UPDATE_ROOT="https://mirror.sjtu.edu.cn/elan/elan/releases" 19 | # _latest="${_latest:=v3.1.1}" 20 | _latest="${_latest:=__INPUT_VERSION__}" 21 | 22 | #XXX: If you change anything here, please make the same changes in setup_mode.rs 23 | usage() { 24 | cat 1>&2 < Choose a default toolchain to install 40 | --default-toolchain none Do not install any toolchains 41 | EOF 42 | } 43 | 44 | main() { 45 | need_cmd curl 46 | need_cmd awk 47 | need_cmd uname 48 | need_cmd mktemp 49 | need_cmd chmod 50 | need_cmd mkdir 51 | need_cmd rm 52 | need_cmd rmdir 53 | 54 | get_architecture || return 1 55 | local _arch="$RETVAL" 56 | assert_nz "$_arch" "arch" 57 | 58 | local _ext="" 59 | case "$_arch" in 60 | *windows*) 61 | _ext=".exe" 62 | ;; 63 | esac 64 | 65 | local _dir="$(mktemp -d 2>/dev/null || ensure mktemp -d -t elan)" 66 | local _file="$_dir/elan-init$_ext" 67 | 68 | local _ansi_escapes_are_valid=false 69 | if [ -t 2 ]; then 70 | if [ "${TERM+set}" = 'set' ]; then 71 | case "$TERM" in 72 | xterm*|rxvt*|urxvt*|linux*|vt*) 73 | _ansi_escapes_are_valid=true 74 | ;; 75 | esac 76 | fi 77 | fi 78 | 79 | # check if we have to use /dev/tty to prompt the user 80 | local need_tty=yes 81 | for arg in "$@"; do 82 | case "$arg" in 83 | -h|--help) 84 | usage 85 | exit 0 86 | ;; 87 | -y) 88 | # user wants to skip the prompt -- we don't need /dev/tty 89 | need_tty=no 90 | ;; 91 | *) 92 | ;; 93 | esac 94 | done 95 | 96 | if $_ansi_escapes_are_valid; then 97 | printf "\33[1minfo:\33[0m downloading installer\n" 1>&2 98 | else 99 | printf '%s\n' 'info: downloading installer' 1>&2 100 | fi 101 | 102 | ensure mkdir -p "$_dir" 103 | 104 | case "$_arch" in 105 | *windows*) 106 | ensure curl -sSfL "$ELAN_UPDATE_ROOT/download/$_latest/elan-$_arch.zip" > "$_dir/elan-init.zip" 107 | (cd "$_dir"; ensure unzip elan-init.zip; ignore rm elan-init.zip) 108 | ;; 109 | *) 110 | ensure curl -sSfL "$ELAN_UPDATE_ROOT/download/$_latest/elan-$_arch.tar.gz" > "$_dir/elan-init.tar.gz" 111 | (cd "$_dir"; ensure tar xf elan-init.tar.gz; ignore rm elan-init.tar.gz) 112 | ;; 113 | esac 114 | 115 | ensure chmod u+x "$_file" 116 | if [ ! -x "$_file" ]; then 117 | printf '%s\n' "Cannot execute $_file (likely because of mounting /tmp as noexec)." 1>&2 118 | printf '%s\n' "Please copy the file to a location where you can execute binaries and run ./elan-init$_ext." 1>&2 119 | exit 1 120 | fi 121 | 122 | 123 | 124 | if [ "$need_tty" = "yes" ]; then 125 | # The installer is going to want to ask for confirmation by 126 | # reading stdin. This script was piped into `sh` though and 127 | # doesn't have stdin to pass to its children. Instead we're going 128 | # to explicitly connect /dev/tty to the installer's stdin. 129 | if [ ! -t 1 ]; then 130 | err "Unable to run interactively. Run with -y to accept defaults, --help for additional options" 131 | fi 132 | 133 | ignore "$_file" "$@" < /dev/tty 134 | else 135 | ignore "$_file" "$@" 136 | fi 137 | 138 | local _retval=$? 139 | 140 | ignore rm "$_file" 141 | ignore rmdir "$_dir" 142 | 143 | return "$_retval" 144 | } 145 | 146 | get_bitness() { 147 | need_cmd head 148 | # Architecture detection without dependencies beyond coreutils. 149 | # ELF files start out "\x7fELF", and the following byte is 150 | # 0x01 for 32-bit and 151 | # 0x02 for 64-bit. 152 | # The printf builtin on some shells like dash only supports octal 153 | # escape sequences, so we use those. 154 | local _current_exe_head=$(head -c 5 /proc/self/exe ) 155 | if [ "$_current_exe_head" = "$(printf '\177ELF\001')" ]; then 156 | echo 32 157 | elif [ "$_current_exe_head" = "$(printf '\177ELF\002')" ]; then 158 | echo 64 159 | else 160 | err "unknown platform bitness" 161 | fi 162 | } 163 | 164 | get_endianness() { 165 | local cputype=$1 166 | local suffix_eb=$2 167 | local suffix_el=$3 168 | 169 | # detect endianness without od/hexdump, like get_bitness() does. 170 | need_cmd head 171 | need_cmd tail 172 | 173 | local _current_exe_endianness="$(head -c 6 /proc/self/exe | tail -c 1)" 174 | if [ "$_current_exe_endianness" = "$(printf '\001')" ]; then 175 | echo "${cputype}${suffix_el}" 176 | elif [ "$_current_exe_endianness" = "$(printf '\002')" ]; then 177 | echo "${cputype}${suffix_eb}" 178 | else 179 | err "unknown platform endianness" 180 | fi 181 | } 182 | 183 | get_architecture() { 184 | 185 | local _ostype="$(uname -s)" 186 | local _cputype="$(uname -m)" 187 | 188 | if [ "$_ostype" = Linux ]; then 189 | if [ "$(uname -o)" = Android ]; then 190 | local _ostype=Android 191 | fi 192 | if ldd --version 2>&1 | grep -q 'musl'; then 193 | err "musl-based systems are unsupported at the moment" 194 | fi 195 | fi 196 | 197 | if [ "$_ostype" = Darwin -a "$_cputype" = i386 ]; then 198 | # Darwin `uname -s` lies 199 | if sysctl hw.optional.x86_64 | grep -q ': 1'; then 200 | local _cputype=x86_64 201 | fi 202 | fi 203 | 204 | case "$_ostype" in 205 | 206 | Android) 207 | local _ostype=linux-android 208 | ;; 209 | 210 | Linux) 211 | local _ostype=unknown-linux-gnu 212 | ;; 213 | 214 | FreeBSD) 215 | local _ostype=unknown-freebsd 216 | ;; 217 | 218 | NetBSD) 219 | local _ostype=unknown-netbsd 220 | ;; 221 | 222 | DragonFly) 223 | local _ostype=unknown-dragonfly 224 | ;; 225 | 226 | Darwin) 227 | local _ostype=apple-darwin 228 | ;; 229 | 230 | MINGW* | MSYS* | CYGWIN*) 231 | local _ostype=pc-windows-msvc 232 | ;; 233 | 234 | *) 235 | err "unrecognized OS type: $_ostype" 236 | ;; 237 | 238 | esac 239 | 240 | case "$_cputype" in 241 | 242 | i386 | i486 | i686 | i786 | x86) 243 | local _cputype=i686 244 | ;; 245 | 246 | xscale | arm) 247 | local _cputype=arm 248 | if [ "$_ostype" = "linux-android" ]; then 249 | local _ostype=linux-androideabi 250 | fi 251 | ;; 252 | 253 | armv6l) 254 | local _cputype=arm 255 | if [ "$_ostype" = "linux-android" ]; then 256 | local _ostype=linux-androideabi 257 | else 258 | local _ostype="${_ostype}eabihf" 259 | fi 260 | ;; 261 | 262 | armv7l | armv8l) 263 | local _cputype=armv7 264 | if [ "$_ostype" = "linux-android" ]; then 265 | local _ostype=linux-androideabi 266 | else 267 | local _ostype="${_ostype}eabihf" 268 | fi 269 | ;; 270 | 271 | arm64 | aarch64) 272 | local _cputype=aarch64 273 | ;; 274 | 275 | x86_64 | x86-64 | x64 | amd64) 276 | local _cputype=x86_64 277 | ;; 278 | 279 | mips) 280 | local _cputype="$(get_endianness $_cputype "" 'el')" 281 | ;; 282 | 283 | mips64) 284 | local _bitness="$(get_bitness)" 285 | if [ $_bitness = "32" ]; then 286 | if [ $_ostype = "unknown-linux-gnu" ]; then 287 | # 64-bit kernel with 32-bit userland 288 | # endianness suffix is appended later 289 | local _cputype=mips 290 | fi 291 | else 292 | # only n64 ABI is supported for now 293 | local _ostype="${_ostype}abi64" 294 | fi 295 | 296 | local _cputype="$(get_endianness $_cputype "" 'el')" 297 | ;; 298 | 299 | ppc) 300 | local _cputype=powerpc 301 | ;; 302 | 303 | ppc64) 304 | local _cputype=powerpc64 305 | ;; 306 | 307 | ppc64le) 308 | local _cputype=powerpc64le 309 | ;; 310 | 311 | *) 312 | err "unknown CPU type: $_cputype" 313 | 314 | esac 315 | 316 | # Detect 64-bit linux with 32-bit userland 317 | if [ $_ostype = unknown-linux-gnu -a $_cputype = x86_64 ]; then 318 | if [ "$(get_bitness)" = "32" ]; then 319 | local _cputype=i686 320 | fi 321 | fi 322 | 323 | # Detect armv7 but without the CPU features Lean needs in that build, 324 | # and fall back to arm. 325 | # See https://github.com/rust-lang-nursery/rustup.rs/issues/587. 326 | if [ $_ostype = "unknown-linux-gnueabihf" -a $_cputype = armv7 ]; then 327 | if ensure grep '^Features' /proc/cpuinfo | grep -q -v neon; then 328 | # At least one processor does not have NEON. 329 | local _cputype=arm 330 | fi 331 | fi 332 | 333 | local _arch="$_cputype-$_ostype" 334 | 335 | RETVAL="$_arch" 336 | } 337 | 338 | say() { 339 | echo "elan: $1" 340 | } 341 | 342 | err() { 343 | say "$1" >&2 344 | exit 1 345 | } 346 | 347 | need_cmd() { 348 | if ! check_cmd "$1" 349 | then err "need '$1' (command not found)" 350 | fi 351 | } 352 | 353 | check_cmd() { 354 | command -v "$1" > /dev/null 2>&1 355 | return $? 356 | } 357 | 358 | need_ok() { 359 | if [ $? != 0 ]; then err "$1"; fi 360 | } 361 | 362 | assert_nz() { 363 | if [ -z "$1" ]; then err "assert_nz $2"; fi 364 | } 365 | 366 | # Run a command that should never fail. If the command fails execution 367 | # will immediately terminate with an error showing the failing 368 | # command. 369 | ensure() { 370 | "$@" 371 | need_ok "command failed: $*" 372 | } 373 | 374 | # This is just for indicating that commands' results are being 375 | # intentionally ignored. Usually, because it's being executed 376 | # as part of error handling. 377 | ignore() { 378 | "$@" 379 | } 380 | 381 | main "$@" || exit 1 382 | -------------------------------------------------------------------------------- /embed/init.go: -------------------------------------------------------------------------------- 1 | package embed 2 | 3 | import ( 4 | _ "embed" 5 | "runtime" 6 | ) 7 | 8 | //go:embed elan-init.ps1 9 | var InitWindows string 10 | 11 | //go:embed elan-init.sh 12 | var InitUnix string 13 | 14 | func InitScriptName() string { 15 | switch runtime.GOOS { 16 | case "windows": 17 | return "elan-init.ps1" 18 | default: 19 | return "elan-init.sh" 20 | } 21 | } 22 | 23 | func InitScriptBytes() string { 24 | switch runtime.GOOS { 25 | case "windows": 26 | return InitWindows 27 | default: 28 | return InitUnix 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /glean/config.go: -------------------------------------------------------------------------------- 1 | package glean 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | const ( 12 | gleanVersion = "v0.1.19" 13 | 14 | urlBase = "https://mirror.sjtu.edu.cn/elan" 15 | ) 16 | 17 | var ( 18 | Command = flag.String("install", "", "available: `elan`, `lean`") 19 | version = flag.String("version", "", "example Elan version: `v3.1.1` (with 'v' prefix); example Lean version: `4.1.0`, `4.2.0-rc2` (without the `v` prefix)") 20 | LakeManifestPath = flag.String("lake-manifest-path", "./lake-manifest.json", "") 21 | Update = flag.Bool("update", false, "update glean") 22 | ) 23 | 24 | var ( 25 | dotElanBaseDir = getDotElanBaseDir() 26 | ) 27 | 28 | func getDotElanBaseDir() string { 29 | userHomeDir, err := os.UserHomeDir() 30 | if err != nil { 31 | panic("Failed to get `UserHomeDir`") 32 | } 33 | dotElanBaseDir := filepath.Join(userHomeDir, ".elan") 34 | log.Println("got dotElanBaseDir `" + dotElanBaseDir + "`") 35 | return dotElanBaseDir 36 | } 37 | 38 | func InitFlags() { 39 | flag.Usage = func() { 40 | fmt.Println("glean " + gleanVersion) 41 | fmt.Println("example usage:") 42 | fmt.Println("\tglean -install elan -version 3.1.1") 43 | fmt.Println("\tglean -install lean -version 4.6.0-rc1") 44 | fmt.Println("Please refer to `https://mirror.sjtu.edu.cn/elan/?mirror_intel_list` for available versions") 45 | _, err := fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", os.Args[0]) 46 | if err != nil { 47 | panic("") 48 | } 49 | flag.PrintDefaults() 50 | } 51 | flag.Parse() 52 | if *Command != "" && *version == "" { 53 | fmt.Println("invalid version") 54 | flag.Usage() 55 | } 56 | log.Printf("flag.Parse: got command = `%s`, version = `%s`", *Command, *version) 57 | } 58 | -------------------------------------------------------------------------------- /glean/elan.go: -------------------------------------------------------------------------------- 1 | package glean 2 | 3 | import ( 4 | "fmt" 5 | "github.com/alissa-tung/glean/embed" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "path" 10 | "path/filepath" 11 | "runtime" 12 | "strings" 13 | ) 14 | 15 | func InstallElan() { 16 | cwd, err := os.Getwd() 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | scriptName := "glean.tmp." + embed.InitScriptName() 22 | scriptPath := filepath.Join(cwd, scriptName) 23 | log.Println("write init script to `" + scriptPath + "`") 24 | file, err := os.Create(scriptPath) 25 | if err != nil { 26 | panic("Failed to create `" + scriptName + "`, " + err.Error()) 27 | } 28 | 29 | func() { 30 | defer func(file *os.File) { 31 | err := file.Close() 32 | if err != nil { 33 | return 34 | } 35 | }(file) 36 | _, err = file.WriteString(strings.Replace(embed.InitScriptBytes(), "__INPUT_VERSION__", *version, 1)) 37 | if err != nil { 38 | panic(err) 39 | } 40 | }() 41 | 42 | var cmd *exec.Cmd 43 | switch runtime.GOOS { 44 | case "windows": 45 | cmd = exec.Command("powershell", "-ExecutionPolicy", "Bypass", "-f", scriptPath) 46 | default: 47 | cmd = exec.Command("/bin/sh", scriptPath, "-y", "--default-toolchain", "none") 48 | 49 | zshText := ` 50 | ######## 51 | # begin @generated by glean 52 | PATH="$HOME/.elan/bin:$PATH" 53 | # end @generated by glean 54 | ######## 55 | ` 56 | zprofilePath := path.Join(os.Getenv("HOME"), ".zprofile") 57 | log.Println("Adding glean to zsh PATH") 58 | file, err := os.OpenFile(zprofilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 59 | if err != nil { 60 | log.Fatalf("append to zprofile error: " + err.Error()) 61 | } 62 | _, err = file.WriteString(zshText) 63 | if err != nil { 64 | log.Fatalf("append to zprofile error: " + err.Error()) 65 | } 66 | } 67 | 68 | log.Println("exec `" + cmd.String() + "`") 69 | 70 | o, err := cmd.CombinedOutput() 71 | if err != nil { 72 | log.Println(string(o)) 73 | panic(err) 74 | } 75 | fmt.Println(string(o)) 76 | // No defer makes user able to run script manully when panic. 77 | _ = os.Remove(scriptPath) 78 | } 79 | -------------------------------------------------------------------------------- /glean/lake.go: -------------------------------------------------------------------------------- 1 | package glean 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "strings" 13 | ) 14 | 15 | type gitRepoSourceMirrorMapping struct { 16 | sourceUrl string 17 | mirrorUrl string 18 | } 19 | 20 | func buildSjtuUrl(pkgName string) string { 21 | return fmt.Sprintf("https://mirror.sjtu.edu.cn/git/lean4-packages/%s", pkgName) 22 | } 23 | 24 | func getRepoName(url string) string { 25 | xs := strings.Split(url, "/") 26 | x := xs[len(xs)-1] 27 | return x 28 | } 29 | 30 | func buildSourceToSjtu(url string) gitRepoSourceMirrorMapping { 31 | return gitRepoSourceMirrorMapping{ 32 | url, 33 | buildSjtuUrl(getRepoName(url)), 34 | } 35 | } 36 | 37 | var ( 38 | repos = [...]string{ 39 | "https://github.com/JLimperg/aesop", 40 | "https://github.com/leanprover-community/aesop", 41 | "https://github.com/leanprover/doc-gen4", 42 | "https://github.com/leanprover/lean4-cli", 43 | "https://github.com/mhuisi/lean4-cli", 44 | "https://github.com/avigad/mathematics_in_lean_source", 45 | "https://github.com/leanprover-community/mathlib4", 46 | "https://github.com/leanprover-community/ProofWidgets4", 47 | "https://github.com/EdAyers/ProofWidgets4", 48 | "https://github.com/gebner/quote4", 49 | "https://github.com/leanprover-community/quote4", 50 | "https://github.com/leanprover/std4", 51 | "https://github.com/leanprover-community/import-graph", 52 | "https://github.com/leanprover-community/batteries", 53 | } 54 | 55 | mirrorRepos = func() []gitRepoSourceMirrorMapping { 56 | var ret []gitRepoSourceMirrorMapping 57 | for _, v := range repos { 58 | ret = append(ret, buildSourceToSjtu(v)) 59 | } 60 | return ret 61 | }() 62 | ) 63 | 64 | func projectDir() string { 65 | return filepath.Dir(*LakeManifestPath) 66 | } 67 | 68 | type lakePackage struct { 69 | Url string `json:"url"` 70 | Rev string `json:"rev"` 71 | Name string `json:"name"` 72 | InputRev string `json:"inputRev"` 73 | } 74 | 75 | type lakeManifest struct { 76 | Version interface{} `json:"version"` 77 | PackagesDir string `json:"packagesDir"` 78 | Packages []lakePackage `json:"packages"` 79 | LakeDir string `json:"lakeDir"` 80 | } 81 | 82 | func readAndParse(url string) lakeManifest { 83 | file, err := os.ReadFile(url) 84 | if err != nil { 85 | fmt.Println("reading `" + url + "`, " + err.Error()) 86 | os.Exit(0) 87 | } 88 | 89 | var obj lakeManifest 90 | if err = json.Unmarshal(file, &obj); err != nil { 91 | panic("error parsing lake manifest `" + url + "`, " + err.Error()) 92 | } 93 | return obj 94 | } 95 | 96 | type lakePackageWithAlias struct { 97 | lakePkg lakePackage 98 | alias *string 99 | } 100 | 101 | func LakeSyncPackages() { 102 | obj := readAndParse(*LakeManifestPath) 103 | 104 | var reposToClone []lakePackageWithAlias 105 | 106 | if len(obj.Packages) == 0 { 107 | panic("empty packages in manifest json") 108 | } 109 | for _, v := range obj.Packages { 110 | mirrorUrl, alias := findMirror(v.Url) 111 | if mirrorUrl != "" { 112 | v.Url = mirrorUrl 113 | reposToClone = append(reposToClone, lakePackageWithAlias{ 114 | lakePkg: v, 115 | alias: alias, 116 | }) 117 | } else { 118 | log.Println("failed to find mirror for: `" + v.Url + "`") 119 | } 120 | } 121 | 122 | for _, pkgWith := range reposToClone { 123 | v := pkgWith.lakePkg 124 | log.Printf("repo to clone: %v\n", v.Name) 125 | 126 | var target string 127 | if pkgWith.alias == nil { 128 | target = filepath.Join(projectDir(), obj.PackagesDir, v.Name) 129 | } else { 130 | target = filepath.Join(projectDir(), obj.PackagesDir, *pkgWith.alias) 131 | } 132 | 133 | if err := os.RemoveAll(target); err != nil { 134 | log.Println("Failed to remove `" + target + "`, " + err.Error()) 135 | } 136 | cmd := exec.Command("git", "clone", v.Url, target) 137 | if err := cmd.Run(); err != nil { 138 | panic("Failed to clone `" + cmd.String() + "`, " + err.Error()) 139 | } 140 | cmd = exec.Command("git", "-C", target, "checkout", v.Rev) 141 | if err := cmd.Run(); err != nil { 142 | panic("Failed to checkout `" + cmd.String() + "`, " + err.Error()) 143 | } 144 | if v.Name == "proofwidgets" { 145 | fmt.Println("Fetching ProofWidgets4 cloud release to " + filepath.Join(target, obj.PackagesDir)) 146 | FetchProofWidgetsRelease(v.InputRev, filepath.Join(target, obj.LakeDir)) 147 | } 148 | } 149 | } 150 | 151 | func FetchProofWidgetsRelease(version string, path string) { 152 | resourceUrl := urlBase + "/proofwidgets/releases/download/" + version + "?mirror_intel_list" 153 | fmt.Println("Fetching from " + resourceUrl) 154 | response, err := http.Get(resourceUrl) 155 | if err != nil { 156 | panic("http.Get error: " + err.Error() + ", resourceUrl = `" + resourceUrl + "`") 157 | } 158 | redirectUrl := response.Request.URL.String() 159 | finalUrl := strings.Replace(redirectUrl, "mirror_clone_list.html", "ProofWidgets4.tar.gz", 1) 160 | log.Println("will get `" + finalUrl + "`") 161 | response, err = http.Get(finalUrl) 162 | if err != nil { 163 | panic("http.Get error: " + err.Error() + ", resourceUrl = `" + finalUrl + "`") 164 | } 165 | defer func(Body io.ReadCloser) { 166 | err := Body.Close() 167 | if err != nil { 168 | return 169 | } 170 | }(response.Body) 171 | 172 | if response.StatusCode != http.StatusOK { 173 | panic("http.Get error") 174 | } 175 | defer func(Body io.ReadCloser) { 176 | err := Body.Close() 177 | if err != nil { 178 | return 179 | } 180 | }(response.Body) 181 | err = os.Mkdir(path, os.ModePerm) 182 | if os.IsExist(err) { 183 | fmt.Println(path + "is already exist") 184 | } 185 | filePath := filepath.Join(path, "ProofWidgets4.tar.gz") 186 | file, err := os.Create(filePath) 187 | if err != nil { 188 | panic("os.Create: " + err.Error() + ", " + "filePath = `" + filePath + "`") 189 | } 190 | defer func(file *os.File) { 191 | err := file.Close() 192 | if err != nil { 193 | return 194 | } 195 | }(file) 196 | _, err = io.Copy(file, response.Body) 197 | if err != nil { 198 | panic(err) 199 | } 200 | _ = file.Close() 201 | } 202 | 203 | func findMirror(url string) (string, *string) { 204 | for _, v := range mirrorRepos { 205 | if v.sourceUrl == url || v.sourceUrl+".git" == url { 206 | return v.mirrorUrl, nil 207 | } 208 | } 209 | return "", nil 210 | } 211 | -------------------------------------------------------------------------------- /glean/lean.go: -------------------------------------------------------------------------------- 1 | package glean 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/http" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "runtime" 12 | "strings" 13 | ) 14 | 15 | func InstallLean() { 16 | releaseName := buildReleaseName(*version) 17 | var resourceUrl string 18 | if strings.Contains(*version, "nightly") { 19 | nightlyIndex := strings.Index(*version, "nightly") 20 | nightlyVersion := *version 21 | resourceUrl = urlBase + "/leanprover/lean4_nightly/releases/download/" + nightlyVersion[nightlyIndex:] + "?mirror_intel_list" 22 | } else { 23 | resourceUrl = urlBase + "/leanprover/lean4/releases/download/v" + *version + "?mirror_intel_list" 24 | } 25 | log.Println("will get `" + resourceUrl + "`") 26 | 27 | response, err := http.Get(resourceUrl) 28 | if err != nil { 29 | panic("http.Get error: " + err.Error() + ", resourceUrl = `" + resourceUrl + "`") 30 | } 31 | defer func(Body io.ReadCloser) { 32 | err := Body.Close() 33 | if err != nil { 34 | return 35 | } 36 | }(response.Body) 37 | 38 | redirectUrl := response.Request.URL.String() 39 | finalUrl := strings.Replace(redirectUrl, "mirror_clone_list.html", releaseName, 1) 40 | log.Println("will get `" + finalUrl + "`") 41 | response, err = http.Get(finalUrl) 42 | if err != nil { 43 | panic("http.Get error: " + err.Error() + ", resourceUrl = `" + finalUrl + "`") 44 | } 45 | defer func(Body io.ReadCloser) { 46 | err := Body.Close() 47 | if err != nil { 48 | return 49 | } 50 | }(response.Body) 51 | 52 | finalToolChainDir := filepath.Join(dotElanBaseDir, "toolchains", buildToolChainDirName(*version)) 53 | err = os.MkdirAll(finalToolChainDir, 0755) 54 | if err != nil { 55 | panic(err.Error()) 56 | } 57 | 58 | tmpToolChainDir := filepath.Join(dotElanBaseDir, "toolchains", "tmp") 59 | err = os.MkdirAll(tmpToolChainDir, 0755) 60 | if err != nil { 61 | panic(err.Error()) 62 | } 63 | 64 | filePath := filepath.Join(dotElanBaseDir, "toolchains", releaseName) 65 | log.Println("download contents will be written to `" + filePath + "`") 66 | file, err := os.Create(filePath) 67 | if err != nil { 68 | panic("os.Create: " + err.Error() + ", " + "filePath = `" + filePath + "`") 69 | } 70 | defer func(file *os.File) { 71 | err := file.Close() 72 | if err != nil { 73 | return 74 | } 75 | }(file) 76 | _, err = io.Copy(file, response.Body) 77 | if err != nil { 78 | panic(err) 79 | } 80 | file.Close() 81 | 82 | var cmd *exec.Cmd 83 | _ = os.RemoveAll(finalToolChainDir) 84 | switch runtime.GOOS { 85 | case "windows": 86 | cmd = exec.Command("tar", "-xvf", filePath, "-C", tmpToolChainDir) 87 | default: 88 | cmd = exec.Command("unzip", filePath, "-d", tmpToolChainDir) 89 | } 90 | 91 | log.Println("exec `" + cmd.String() + "`") 92 | 93 | if err := cmd.Run(); err != nil { 94 | panic(err) 95 | } 96 | 97 | oldPath := filepath.Join( 98 | tmpToolChainDir, 99 | fmt.Sprintf("lean-%s-%s", *version, buildReleasePostfix()), 100 | ) 101 | if err := os.Rename(oldPath, finalToolChainDir); err != nil { 102 | panic("Can not rename `" + oldPath + "` to `" + finalToolChainDir + "`") 103 | } 104 | 105 | if err != nil { 106 | return 107 | } 108 | 109 | _ = os.Remove(filePath) 110 | _ = os.RemoveAll(tmpToolChainDir) 111 | } 112 | 113 | func buildReleaseName(version string) string { 114 | name := fmt.Sprintf("lean-%s-%s.zip", version, buildReleasePostfix()) 115 | log.Println("buildReleaseName: `" + name + "`") 116 | return name 117 | } 118 | 119 | func buildReleasePostfix() string { 120 | switch runtime.GOOS { 121 | case "windows": 122 | return "windows" 123 | default: 124 | switch runtime.GOARCH { 125 | case "aarch64": 126 | return runtime.GOOS + "_" + runtime.GOARCH 127 | default: 128 | return runtime.GOOS 129 | } 130 | } 131 | } 132 | 133 | func buildToolChainDirName(version string) string { 134 | if strings.Contains(version, "nightly") { 135 | nightlyVersion := version[strings.Index(version, "nightly"):] 136 | return fmt.Sprintf("leanprover--lean4---%s", nightlyVersion) 137 | } else { 138 | return fmt.Sprintf("leanprover--lean4---v%s", version) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /glean/update.go: -------------------------------------------------------------------------------- 1 | package glean 2 | 3 | import ( 4 | "fmt" 5 | "golang.org/x/text/cases" 6 | "golang.org/x/text/language" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "os" 12 | "os/exec" 13 | "path/filepath" 14 | "runtime" 15 | "strings" 16 | ) 17 | 18 | func buildGleanReleaseName() string { 19 | var arch string 20 | if runtime.GOARCH == "amd64" { 21 | arch = "x86_64" 22 | } else { 23 | arch = "arm64" 24 | } 25 | 26 | switch runtime.GOOS { 27 | case "windows": 28 | name := fmt.Sprintf("glean_%s_%s.zip", "Windows", arch) 29 | return name 30 | default: 31 | caser := cases.Title(language.Und) 32 | name := fmt.Sprintf("glean_%s_%s.tar.gz", caser.String(runtime.GOOS), arch) 33 | return name 34 | } 35 | } 36 | 37 | func GetLatestVersion() string { 38 | 39 | response, err := http.Get(urlBase + "/glean/releases/download/?mirror_intel_list") 40 | if err != nil { 41 | panic(err.Error()) 42 | } 43 | defer response.Body.Close() 44 | 45 | body, err := ioutil.ReadAll(response.Body) 46 | if err != nil { 47 | panic(err.Error()) 48 | } 49 | 50 | bodyText := string(body) 51 | index := strings.Index(bodyText, "v0") 52 | var latestVersion string 53 | for index != -1 { 54 | latestVersion = bodyText[index : index+7] 55 | fmt.Println("Found latest version:", latestVersion) 56 | fmt.Println("run `glean -update` to update") 57 | break 58 | } 59 | return latestVersion 60 | } 61 | 62 | func CheckUpdate() { 63 | fmt.Println("Checking for updates...") 64 | latestVersion := GetLatestVersion() 65 | if latestVersion == gleanVersion { 66 | fmt.Println("Already up to date") 67 | return 68 | } 69 | fmt.Println("New version available:", latestVersion) 70 | 71 | releaseName := buildGleanReleaseName() 72 | releaseUrl := urlBase + "/glean/releases/download/" + latestVersion + "?mirror_intel_list" 73 | response, err := http.Get(releaseUrl) 74 | if err != nil { 75 | panic(err) 76 | } 77 | defer func(Body io.ReadCloser) { 78 | err := Body.Close() 79 | if err != nil { 80 | return 81 | } 82 | }(response.Body) 83 | 84 | redirectUrl := response.Request.URL.String() 85 | finalUrl := strings.Replace(redirectUrl, "mirror_clone_list.html", releaseName, 1) 86 | log.Println("will get `" + finalUrl + "`") 87 | response, err = http.Get(finalUrl) 88 | if err != nil { 89 | panic("http.Get error: " + err.Error() + ", resourceUrl = `" + finalUrl + "`") 90 | } 91 | defer func(Body io.ReadCloser) { 92 | err := Body.Close() 93 | if err != nil { 94 | return 95 | } 96 | }(response.Body) 97 | 98 | filePath := filepath.Join(dotElanBaseDir, "bin", releaseName) 99 | log.Println("download contents will be written to `" + filePath + "`") 100 | file, err := os.Create(filePath) 101 | if err != nil { 102 | panic("os.Create: " + err.Error() + ", " + "filePath = `" + filePath + "`") 103 | } 104 | defer func(file *os.File) { 105 | err := file.Close() 106 | if err != nil { 107 | return 108 | } 109 | }(file) 110 | _, err = io.Copy(file, response.Body) 111 | if err != nil { 112 | panic(err.Error()) 113 | } 114 | file.Close() 115 | var cmd *exec.Cmd 116 | gleantmpPath := filepath.Join(dotElanBaseDir, "bin", "glean.new") 117 | err = os.MkdirAll(gleantmpPath, 0755) 118 | if err != nil { 119 | panic(err.Error()) 120 | } 121 | 122 | cmd = exec.Command("tar", "-xvf", filePath, "-C", gleantmpPath) 123 | 124 | if err := cmd.Run(); err != nil { 125 | panic(err.Error()) 126 | } 127 | err = os.Remove(filePath) 128 | if err != nil { 129 | panic(err.Error()) 130 | } 131 | 132 | if runtime.GOOS == "windows" { 133 | fmt.Printf("Please run the command `cp %s\\glean.exe %s`", gleantmpPath, dotElanBaseDir+"\\bin") 134 | os.Exit(0) 135 | } 136 | 137 | os.Remove(dotElanBaseDir + "/bin/glean") 138 | cmd = exec.Command("cp", gleantmpPath+"/glean", dotElanBaseDir+"/bin") 139 | err = cmd.Run() 140 | if err != nil { 141 | panic(err) 142 | } 143 | fmt.Println("glean has been updated to ", latestVersion) 144 | } 145 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/alissa-tung/glean 2 | 3 | go 1.20 4 | 5 | require golang.org/x/text v0.14.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 2 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 3 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/alissa-tung/glean/glean" 7 | ) 8 | 9 | func main() { 10 | glean.InitFlags() 11 | glean.GetLatestVersion() 12 | fmt.Println("Please refer to `https://mirror.sjtu.edu.cn/elan/?mirror_intel_list` for available versions") 13 | if *glean.Update { 14 | glean.CheckUpdate() 15 | } 16 | switch *glean.Command { 17 | case "elan": 18 | glean.InstallElan() 19 | 20 | case "lean": 21 | glean.InstallLean() 22 | 23 | default: 24 | if *glean.LakeManifestPath != "" { 25 | glean.LakeSyncPackages() 26 | } else { 27 | fmt.Println("unknown command") 28 | flag.Usage() 29 | } 30 | } 31 | } 32 | --------------------------------------------------------------------------------