├── .gitignore ├── index.js ├── install-rstats.js ├── lib ├── create-lib.sh ├── download-files.js ├── download-maybe.js ├── get-all-rtools-urls.js ├── get-major-version.js ├── get-minor-version.js ├── install-macos.js ├── install-win32.js ├── install.js ├── installer.bat ├── installer.sh ├── resolve-all-versions.js ├── resolve-url.js ├── resolve-version.js ├── rtools-version-needed.js ├── shcuts.bat ├── tempfile.js ├── url-macos.js ├── url-win.js └── urls.js ├── news.md ├── package-lock.json ├── package.json ├── readme.md └── tests ├── test-download-files.js ├── test-download-maybe.js ├── test-macos.js ├── test-resolve-all-versions.js ├── test-resolve-url.js ├── test-tempfile.js └── test-versions.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | const install = require('./lib/install'); 3 | 4 | module.exports = { 5 | install: install 6 | }; 7 | -------------------------------------------------------------------------------- /install-rstats.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | const meow = require('meow'); 4 | const installr = require('.'); 5 | 6 | const cli = meow(` 7 | Usage 8 | $ install-rstats [rversion1] [rversion2] ... 9 | 10 | Supported R versions: 11 | 'release': the current R release 12 | 'oldrel': the previous minor branch 13 | 'devel': development snapshot 14 | 'x.y': last released version of the x.y branch 15 | 'x.y.z': version x.y.z 16 | `); 17 | 18 | installr.install(cli.input); 19 | -------------------------------------------------------------------------------- /lib/create-lib.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | sourced=0 4 | if [ -n "$ZSH_EVAL_CONTEXT" ]; then 5 | case $ZSH_EVAL_CONTEXT in *:file) sourced=1;; esac 6 | elif [ -n "$KSH_VERSION" ]; then 7 | [ "$(cd $(dirname -- $0) && pwd -P)/$(basename -- $0)" != "$(cd $(dirname -- ${.sh.file}) && pwd -P)/$(basename -- ${.sh.file})" ] && sourced=1 8 | elif [ -n "$BASH_VERSION" ]; then 9 | (return 0 2>/dev/null) && sourced=1 10 | else 11 | # All other shells: examine $0 for known shell binary filenames 12 | # Detects `sh` and `dash`; add additional shell filenames as needed. 13 | case ${0##*/} in sh|dash) sourced=1;; esac 14 | fi 15 | 16 | function vercomp () { 17 | if [[ $1 == $2 ]] 18 | then 19 | return 0 20 | fi 21 | local IFS=. 22 | local i ver1=($1) ver2=($2) 23 | # fill empty fields in ver1 with zeros 24 | for ((i=${#ver1[@]}; i<${#ver2[@]}; i++)) 25 | do 26 | ver1[i]=0 27 | done 28 | for ((i=0; i<${#ver1[@]}; i++)) 29 | do 30 | if [[ -z ${ver2[i]} ]] 31 | then 32 | # fill empty fields in ver2 with zeros 33 | ver2[i]=0 34 | fi 35 | if ((10#${ver1[i]} > 10#${ver2[i]})) 36 | then 37 | return 1 38 | fi 39 | if ((10#${ver1[i]} < 10#${ver2[i]})) 40 | then 41 | return 2 42 | fi 43 | done 44 | return 0 45 | } 46 | 47 | function create_libs() { 48 | local vers=$(installed_r_versions) 49 | local base="/Library/Frameworks/R.framework/Versions" 50 | for ver in $vers 51 | do 52 | # from R 4.2.0 R_LIBS_USER is not hard-coded in the config, 53 | # but we need to run R to query it. This works on older versions 54 | # as well, but for those we can get away without starting R, so 55 | # we don't. 56 | if [[ "`(vercomp $ver 4.1.9; echo $?)`" == "1" ]]; then 57 | exec="$base/$ver/Resources/R" 58 | lib=`$exec --vanilla -s -e 'cat(Sys.getenv("R_LIBS_USER"))'` 59 | else 60 | renv="$base/$ver/Resources/etc/Renviron" 61 | lib=$(bash -c "source $renv; echo \$R_LIBS_USER") 62 | fi 63 | # This is to expand the tilde 64 | lib=$(bash -c "echo $lib") 65 | if test -e "$lib"; then 66 | if ! test -d "$lib"; then 67 | echo "Warning: library '$lib' is not a directory" 68 | fi 69 | else 70 | echo "Creating library '$lib'" 71 | mkdir -p $lib || echo "Failed to create '$lib'" 72 | fi 73 | done 74 | } 75 | 76 | function main() { 77 | set -e 78 | local scriptdir=$(dirname $0) 79 | source "$scriptdir/installer.sh" 80 | create_libs 81 | } 82 | 83 | if [ "$sourced" = "0" ]; then 84 | set -e 85 | main "$@" 86 | fi 87 | -------------------------------------------------------------------------------- /lib/download-files.js: -------------------------------------------------------------------------------- 1 | 2 | const download_maybe = require('./download-maybe'); 3 | const ora = require('ora'); 4 | const pretty_bytes = require('pretty-bytes'); 5 | 6 | async function download_files(urls) { 7 | 8 | var done = 0; 9 | const spin = ora('Downloading files: ' + done + '/' + urls.length).start(); 10 | var spinner = { 11 | dls: { }, 12 | show: function() { 13 | var current = 0, total = 0; 14 | for (const url in spinner.dls) { 15 | current += spinner.dls[url].current; 16 | total += spinner.dls[url].total; 17 | } 18 | spin.text = 'Downloading files: ' + done + '/' + 19 | urls.length + ' ' + pretty_bytes(current) + '/' + 20 | pretty_bytes(total); 21 | } 22 | }; 23 | 24 | try { 25 | var pdl = urls.map(async function(url) { 26 | const filename = await download_maybe(url, spinner); 27 | done++; 28 | spinner.show(); 29 | return filename; 30 | }) 31 | 32 | } catch(error) { 33 | spin.fail(); 34 | throw error; 35 | } 36 | 37 | const filenames = await Promise.all(pdl); 38 | spin.succeed(); 39 | return filenames; 40 | } 41 | 42 | module.exports = download_files; 43 | -------------------------------------------------------------------------------- /lib/download-maybe.js: -------------------------------------------------------------------------------- 1 | 2 | const temp = require('temp-dir'); 3 | const got = require('got'); 4 | const mkdirp = require('mkdirp'); 5 | const filenamify = require('filenamify') 6 | 7 | const stream = require('stream'); 8 | const fs = require('fs'); 9 | const path = require('path'); 10 | const {promisify} = require('util'); 11 | 12 | const pipeline = promisify(stream.pipeline); 13 | const unlink = promisify(fs.unlink); 14 | const access = promisify(fs.access); 15 | const rename = promisify(fs.rename); 16 | 17 | async function download_maybe(url, spinner) { 18 | const filename = path.join( 19 | temp, 20 | filenamify(path.basename(url), { replacement: '-' }) 21 | ); 22 | 23 | // If the file exists, we just return it. Otherwise we download it 24 | // It would be great to check that it is a non-broken file somehow... 25 | 26 | try { 27 | await access(filename); 28 | 29 | } catch(err) { 30 | 31 | try { 32 | const str = got.stream(url); 33 | if (spinner !== undefined) { 34 | spinner.dls[url] = { current: 0, total: 0 }; 35 | str.on('downloadProgress', function(data) { 36 | spinner.dls[url].total = data.total; 37 | spinner.dls[url].current = data.transferred; 38 | spinner.show(); 39 | }); 40 | } 41 | await mkdirp(path.dirname(filename)); 42 | await pipeline(str, fs.createWriteStream(filename + '.partial')); 43 | await rename(filename + '.partial', filename); 44 | 45 | } catch(err) { 46 | try { await unlink(filename); } catch(err) { } 47 | try { await unlink(filename + 'partial') } catch (err) { } 48 | throw err; 49 | } 50 | } 51 | 52 | return filename; 53 | } 54 | 55 | module.exports = download_maybe; 56 | -------------------------------------------------------------------------------- /lib/get-all-rtools-urls.js: -------------------------------------------------------------------------------- 1 | 2 | const get_major_version = require('./get-major-version'); 3 | const get_minor_version = require('./get-minor-version'); 4 | const rtools_version_needed = require('./rtools-version-needed'); 5 | const urls = require('./urls'); 6 | 7 | function unique(arr) { 8 | return arr.filter(function(elem, pos) { 9 | return arr.indexOf(elem) == pos; 10 | }); 11 | } 12 | 13 | function get_rtools_url(rversion) { 14 | const major = get_major_version(rversion); 15 | const minor = get_minor_version(rversion); 16 | if (major == 3 && minor <= 2) { 17 | return '35'; // TODO: this should be '33', really 18 | } else if (major == 3) { 19 | return '35'; 20 | } else { 21 | return '40'; 22 | } 23 | } 24 | 25 | async function async_filter(arr, predicate) { 26 | const results = await Promise.all(arr.map(predicate)); 27 | return arr.filter((_v, index) => results[index]); 28 | } 29 | 30 | async function get_all_rtools_urls(rversions) { 31 | const vers = unique(rversions.map(get_rtools_url)); 32 | const versneeded = await async_filter(vers, rtools_version_needed); 33 | const rtoolsurls = versneeded.map(function(x) { return urls.rtools[x]}); 34 | return rtoolsurls; 35 | } 36 | 37 | module.exports = get_all_rtools_urls; 38 | -------------------------------------------------------------------------------- /lib/get-major-version.js: -------------------------------------------------------------------------------- 1 | 2 | function get_major_version(x) { 3 | return x.replace(/^([0-9]+)\..*$/, '$1'); 4 | } 5 | 6 | module.exports = get_major_version; 7 | -------------------------------------------------------------------------------- /lib/get-minor-version.js: -------------------------------------------------------------------------------- 1 | 2 | function get_minor_version(x) { 3 | return x.replace(/^[0-9]+\.([0-9]+).*$/, '$1'); 4 | } 5 | 6 | module.exports = get_minor_version; 7 | -------------------------------------------------------------------------------- /lib/install-macos.js: -------------------------------------------------------------------------------- 1 | 2 | const resolve_all_versions = require('./resolve-all-versions'); 3 | const url_macos = require('./url-macos'); 4 | const download_files = require('./download-files'); 5 | const {promisify} = require('util'); 6 | const sudo = promisify(require('sudo-prompt').exec); 7 | const execa = require('execa'); 8 | const ora = require('ora'); 9 | const Tail = require('tail').Tail; 10 | const path = require('path'); 11 | const tempfile = require('./tempfile'); 12 | 13 | async function install_macos(versions) { 14 | 15 | const rversions = await resolve_all_versions(versions, 'macos'); 16 | const urls = rversions.map(url_macos); 17 | const filenames = await download_files(urls); 18 | 19 | // Run install script to do the rest 20 | const vs = rversions.join(", "); 21 | const script = path.join(__dirname, "/installer.sh"); 22 | const outfile = await tempfile(); 23 | 24 | const spin = ora('Installing R version(s): ' + vs).start(); 25 | spin.info(); 26 | 27 | // Maybe we don't need a password for sudo? 28 | const sudo_test = await execa('sudo', ['-n', 'true'], { reject: false }); 29 | if (sudo_test.failed) { 30 | var tail = new Tail(outfile); 31 | tail.on('line', function(data) { console.log("→ " + data); }); 32 | try { 33 | await sudo( 34 | script + ' ' + filenames.join(" ") + " 2>&1 >> " + outfile, 35 | { name: 'installrstats' } 36 | ) 37 | } catch(error) { 38 | tail.unwatch() 39 | throw error; 40 | } 41 | tail.unwatch() 42 | } else { 43 | await execa( 44 | 'sudo', 45 | [script].concat(filenames), 46 | { stdout: 'inherit', stderr: 'inherit'} 47 | ) 48 | } 49 | 50 | const script2 = path.join(__dirname, "/create-lib.sh"); 51 | const {stdout} = await execa(script2); 52 | console.log(stdout); 53 | } 54 | 55 | module.exports = install_macos; 56 | -------------------------------------------------------------------------------- /lib/install-win32.js: -------------------------------------------------------------------------------- 1 | 2 | const resolve_all_versions = require('./resolve-all-versions'); 3 | const get_all_rtools_urls = require('./get-all-rtools-urls'); 4 | const download_files = require('./download-files'); 5 | 6 | const url_win = require('./url-win'); 7 | const ora = require('ora'); 8 | const {promisify} = require('util'); 9 | const sudo = promisify(require('sudo-prompt').exec); 10 | const path = require('path'); 11 | const execa = require('execa'); 12 | 13 | async function install_win32(versions) { 14 | 15 | const rversions = await resolve_all_versions(versions, 'win'); 16 | const urls = await Promise.all(rversions.map(url_win)); 17 | const rtoolsurls = await get_all_rtools_urls(rversions); 18 | 19 | const allurls = urls.concat(rtoolsurls); 20 | const allfilenames = await download_files(allurls); 21 | 22 | const vs = rversions.join(", "); 23 | var rtvs = ""; 24 | if (rtoolsurls.length == 1) { 25 | rtvs = ", and Rtools" 26 | } else if (rtoolsurls.length > 1) { 27 | rtvs = ", and " + rtoolsurls.length + " Rtools versions" 28 | } 29 | 30 | const spin = ora( 31 | 'Installing R version(s): ' + vs + rtvs + ". This will take several minutes." 32 | ).start(); 33 | spin.info(); 34 | 35 | const wd = process.cwd() 36 | process.chdir(__dirname); 37 | 38 | const script = path.join(__dirname, "/installer.bat"); 39 | try { 40 | if (process.env.GITHUB_ACTION !== undefined) { 41 | const options = { 'stdout': 'inherit', 'stderr': 'inherit' } 42 | await execa(script, allfilenames, options); 43 | } else { 44 | await sudo( 45 | script + ' ' + allfilenames.join(" "), 46 | { name: 'installrstats' } 47 | ) 48 | } 49 | spin.succeed() 50 | 51 | } catch(error) { 52 | spin.fail() 53 | throw error; 54 | 55 | } finally { 56 | process.chdir(wd); 57 | } 58 | } 59 | 60 | module.exports = install_win32; 61 | -------------------------------------------------------------------------------- /lib/install.js: -------------------------------------------------------------------------------- 1 | 2 | const install_win32 = require('./install-win32'); 3 | const install_macos = require('./install-macos'); 4 | 5 | async function install(versions) { 6 | versions = versions || []; 7 | if (versions.length == 0) { versions = ['release']; } 8 | if (process.platform === "win32") { 9 | return install_win32(versions); 10 | } else if (process.platform === "darwin") { 11 | return install_macos(versions); 12 | } else { 13 | throw new Error("Unsupported OS, only Windows and macOS are supported"); 14 | } 15 | } 16 | 17 | module.exports = install; 18 | -------------------------------------------------------------------------------- /lib/installer.bat: -------------------------------------------------------------------------------- 1 | 2 | @echo off 3 | setlocal enableDelayedExpansion 4 | 5 | set argCount=0 6 | for %%x in (%*) do set /A argCount+=1 7 | 8 | set /a counter=0 9 | for /l %%x in (1, 1, %argCount%) do ( 10 | set /a counter=!counter!+1 11 | call echo Running %%!counter! 12 | call %%!counter! /VERYSILENT /SUPPRESSMSGBOXES 13 | ) 14 | 15 | call shcuts.bat 16 | 17 | endlocal 18 | -------------------------------------------------------------------------------- /lib/installer.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | sourced=0 4 | if [ -n "$ZSH_EVAL_CONTEXT" ]; then 5 | case $ZSH_EVAL_CONTEXT in *:file) sourced=1;; esac 6 | elif [ -n "$KSH_VERSION" ]; then 7 | [ "$(cd $(dirname -- $0) && pwd -P)/$(basename -- $0)" != "$(cd $(dirname -- ${.sh.file}) && pwd -P)/$(basename -- ${.sh.file})" ] && sourced=1 8 | elif [ -n "$BASH_VERSION" ]; then 9 | (return 0 2>/dev/null) && sourced=1 10 | else 11 | # All other shells: examine $0 for known shell binary filenames 12 | # Detects `sh` and `dash`; add additional shell filenames as needed. 13 | case ${0##*/} in sh|dash) sourced=1;; esac 14 | fi 15 | 16 | function installed_r_versions() { 17 | ls /Library/Frameworks/R.framework/Versions | 18 | tr -d / | 19 | grep '^[0-9][0-9]*\.[0-9][0-9]*' 20 | } 21 | 22 | # Install all downloaded pkg files 23 | function install_pkg() { 24 | local pkg="" 25 | for pkg in "$@" 26 | do 27 | echo "Installing ${pkg}" 28 | forget_r_packages 29 | installer -pkg "$pkg" -target / 30 | forget_r_packages 31 | done 32 | } 33 | 34 | function update_access_rights() { 35 | ( 36 | local id=$(id -u) 37 | if [[ "$id" != "0" ]]; then 38 | echo "You need sudo to update access rights" 39 | exit 1 40 | fi 41 | local vers=$(installed_r_versions) 42 | for ver in $vers 43 | do 44 | chmod -R g-w "/Library/Frameworks/R.framework/Versions/$ver" 45 | done 46 | ) 47 | } 48 | 49 | function update_quick_links() { 50 | local base=/Library/Frameworks/R.framework/Versions/ 51 | 52 | # Check that all installed R versions have quick links 53 | local vers=$(installed_r_versions) 54 | for ver in $vers 55 | do 56 | local linkfile="/usr/local/bin/R-$ver" 57 | local target="${base}${ver}/Resources/bin/R" 58 | if [[ ! -e "$linkfile" ]]; then 59 | echo Creating quick link for R-${ver}... 60 | ln -s "$target" "$linkfile" 61 | elif [[ ! -L "$linkfile" ]]; then 62 | echo File "$linkfile" exists, but it is not a symlink 63 | else 64 | local current=$(readlink "$linkfile") 65 | if [[ "$current" != "$target" ]]; then 66 | echo Link "$linkfile" exists, but its target is wrong 67 | fi 68 | fi 69 | done 70 | 71 | # Check for dangling links 72 | local links=$(find /usr/local/bin -regex '^R-[0-9][0-9]*\.[0-9][0-9]*') 73 | for link in $links 74 | do 75 | if [[ ! -L "$link" ]]; then 76 | echo Skipping "$link", it is not a symlink 77 | else 78 | local current=$(readlink "$link") 79 | if [[ ! -e "$current" ]]; then 80 | echo Cleaning up dangling link "$link" 81 | rm "$link" 82 | fi 83 | fi 84 | done 85 | } 86 | 87 | function forget_r_packages() { 88 | local pkgs=$(pkgutil --pkgs | grep -i r-project | grep -v clang) 89 | local pkg="" 90 | for pkg in $pkgs 91 | do 92 | pkgutil --forget "$pkg" || true 93 | done 94 | } 95 | 96 | function make_orthogonal() { 97 | local base=/Library/Frameworks/R.framework/Versions/ 98 | local vers=$(installed_r_versions) 99 | for ver in $vers 100 | do 101 | local rfile="${base}${ver}/Resources/bin/R" 102 | if grep -q 'R.framework/Resources' "$rfile"; then 103 | echo "Making R $ver orthogonal" 104 | cat "$rfile" | 105 | sed 's/R.framework\/Resources/R.framework\/Versions\/'$ver'\/Resources/' \ 106 | > "${rfile}.new" 107 | mv "${rfile}.new" "$rfile" 108 | chmod +x "$rfile" 109 | fi 110 | 111 | local rfile="${base}${ver}/Resources/etc/Renviron" 112 | if grep -q 'R.framework/Resources' "$rfile"; then 113 | cat "$rfile" | 114 | sed 's/R.framework\/Resources/R.framework\/Versions\/'$ver'\/Resources/' \ 115 | > "${rfile}.new" 116 | mv "${rfile}.new" "$rfile" 117 | fi 118 | 119 | local rfile="${base}${ver}/Resources/fontconfig/fonts/fonts.conf" 120 | if grep -q 'R.framework/Resources' "$rfile"; then 121 | cat "$rfile" | 122 | sed 's/R.framework\/Resources/R.framework\/Versions\/'$ver'\/Resources/' \ 123 | > "${rfile}.new" 124 | mv "${rfile}.new" "$rfile" 125 | fi 126 | 127 | local rfile="${base}${ver}/Resources/etc/Makeconf" 128 | if grep -q 'F/Library/Frameworks/R\.framework/\.\.' "$rfile"; then 129 | cat "$rfile" | 130 | sed 's/-F\/Library\/Frameworks\/R\.framework\/\.\./-F\/Library\/Frameworks\/R.framework\/Versions\/'$ver'/' \ 131 | > "${rfile}.new" 132 | mv "${rfile}.new" "$rfile" 133 | fi 134 | 135 | local fake="${base}${ver}/R.framework" 136 | mkdir -p "$fake" 137 | ln -s ../Headers "$fake/Headers" 2> /dev/null || true 138 | ln -s ../Resources/lib "$fake/Libraries" 2> /dev/null || true 139 | ln -s ../PrivateHeaders "$fake/PrivateHeaders" 2> /dev/null || true 140 | ln -s ../R "$fake/R" 2> /dev/null || true 141 | ln -s ../Resources "$fake/Resources" 2> /dev/null || true 142 | done 143 | } 144 | 145 | function main() { 146 | set -e 147 | install_pkg "$@" 148 | make_orthogonal 149 | update_access_rights 150 | update_quick_links 151 | } 152 | 153 | if [ "$sourced" = "0" ]; then 154 | set -e 155 | main "$@" 156 | fi 157 | -------------------------------------------------------------------------------- /lib/resolve-all-versions.js: -------------------------------------------------------------------------------- 1 | 2 | const resolve_version = require('./resolve-version'); 3 | const ora = require('ora'); 4 | const unique = require('array-unique'); 5 | 6 | async function resolve_all_versions(versions, os) { 7 | var result; 8 | 9 | const spin = ora('Resolving ' + versions.length + ' R version(s)') 10 | .start(); 11 | 12 | try { 13 | var pversions = versions.map( 14 | function(v) { return resolve_version(v, os); } 15 | ); 16 | const result = await Promise.all(pversions); 17 | spin.succeed(); 18 | return unique(result); 19 | } catch(error) { 20 | spin.fail(); 21 | throw error; 22 | } 23 | } 24 | 25 | module.exports = resolve_all_versions; 26 | -------------------------------------------------------------------------------- /lib/resolve-url.js: -------------------------------------------------------------------------------- 1 | 2 | const rversions = require('rversions'); 3 | const api = process.env.R_VERSIONS_API_URL || 4 | 'https://api.r-hub.io/rversions/'; 5 | const got = require('got'); 6 | 7 | async function resolve_rv(endpoint) { 8 | const mp = { 9 | 'r-release': 'r_release', 10 | 'r-release-macos': 'r_release_macos', 11 | 'r-release-win': 'r_release_win', 12 | 'r-oldrel': 'r_oldrel', 13 | 'r-versions': 'r_versions' 14 | }; 15 | const mb = mp[endpoint]; 16 | if (mb === undefined) { 17 | throw(new Error('Unknown rvresions APIx endpoint: ' + endpoint)); 18 | } 19 | return await rversions[mb](); 20 | } 21 | 22 | async function resolve_url(endpoint) { 23 | var body; 24 | try { 25 | body = await got(api + endpoint).json(); 26 | } catch(err) { 27 | body = await resolve_rv(endpoint); 28 | } 29 | 30 | return body; 31 | } 32 | 33 | module.exports = resolve_url; 34 | -------------------------------------------------------------------------------- /lib/resolve-version.js: -------------------------------------------------------------------------------- 1 | 2 | const resolve_url = require('./resolve-url'); 3 | const get_major_version = require('./get-major-version'); 4 | const get_minor_version = require('./get-minor-version'); 5 | 6 | async function resolve_version(version, os = undefined) { 7 | if (version === undefined) { version = 'release'; } 8 | 9 | if (version === 'devel') { 10 | // Do nothing with devel 11 | 12 | } else if (version === 'release') { 13 | let rrls; 14 | if (os === undefined) { 15 | rrls = await resolve_url('r-release'); 16 | } else if (os === 'macos' || os === 'mac' || os === 'macOS' || 17 | os === 'darwin') { 18 | rrls = await resolve_url('r-release-macos'); 19 | } else if (os === 'windows' || os === 'win' || os === 'win32') { 20 | rrls = await resolve_url('r-release-win'); 21 | } else { 22 | throw new Error('Unknown OS in `resolve_version()`: ' + os); 23 | } 24 | 25 | version = rrls.version; 26 | 27 | } else if (version === 'oldrel') { 28 | const rold = await resolve_url('r-oldrel'); 29 | version = rold.version; 30 | 31 | } else if (/^[0-9]+\.[0-9]+$/.test(version)) { 32 | const maj = version.replace(/^([0-9]+)\..*$/, '$1'); 33 | const min = version.replace(/^[0-9]+\.([0-9]+).*$/, '$1'); 34 | const rls = await resolve_url('r-versions'); 35 | const majors = rls.map(function(x) { return get_major_version(x.version); }); 36 | const minors = rls.map(function(x) { return get_minor_version(x.version); }); 37 | let i; 38 | for (i = rls.length - 1; i >= 0; i--) { 39 | if (majors[i] == maj && minors[i] == min) break; 40 | } 41 | 42 | if (i < 0) { 43 | throw new Error('Unknown minor R version: ' + version); 44 | } else { 45 | version = rls[i].version; 46 | } 47 | 48 | } else { 49 | const rls = await resolve_url('r-versions'); 50 | const vrs = rls.map(function(x) { return x.version; }); 51 | if (vrs.indexOf(version) == -1) { 52 | throw new Error('Unknown R version: ' + version); 53 | } 54 | } 55 | 56 | return version; 57 | } 58 | 59 | module.exports = resolve_version; 60 | -------------------------------------------------------------------------------- /lib/rtools-version-needed.js: -------------------------------------------------------------------------------- 1 | 2 | const winreg = require('winreg'); 3 | const bluebird = require('bluebird'); 4 | 5 | async function reg_query(ver, arch) { 6 | const dver = ver[0] + '.' + ver[1]; 7 | const reg = new winreg({ 8 | hive: winreg.HKLM, 9 | key: '\\Software\\R-core\\Rtools\\' + dver, 10 | arch: arch 11 | }); 12 | 13 | bluebird.promisifyAll(reg) 14 | 15 | var values; 16 | try { 17 | values = await reg.valuesAsync(); 18 | } catch(err) { 19 | values = false 20 | } 21 | 22 | return (values !== false); 23 | } 24 | 25 | async function rtools_version_needed(ver) { 26 | const q32 = reg_query(ver, 'x86'); 27 | const q64 = reg_query(ver, 'x64'); 28 | const ret = await Promise.all([q32, q64]); 29 | return !ret[0] && !ret[1]; 30 | } 31 | 32 | module.exports = rtools_version_needed; 33 | -------------------------------------------------------------------------------- /lib/shcuts.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enableDelayedExpansion 3 | 4 | REM ---------------------------------------------------------------- 5 | REM Create the directory that we use for the links 6 | REM ---------------------------------------------------------------- 7 | 8 | SET linkdir=%ProgramFiles% 9 | IF "%linkdir%"=="" ( 10 | SET linkdir=C:\Program Files 11 | ) 12 | 13 | SET linkdir=%linkdir%\R\bin 14 | 15 | IF EXIST "%linkdir%\" ( 16 | REM Exists, good 17 | ) ELSE ( 18 | echo Creating symlink directory: '%linkdir%'. 19 | MKDIR "%linkdir%" 20 | IF %errorlevel% NEQ 0 EXIT /b %errorlevel% 21 | ) 22 | 23 | REM ---------------------------------------------------------------- 24 | REM Add to path. This is a bit tricky to do in a way that 25 | REM we make sure that it is on the system path, and it is also 26 | REM set in the current process. 27 | REM ---------------------------------------------------------------- 28 | 29 | REM Get the system path 30 | REM Explanation: https://stackoverflow.com/a/16282366/604364 31 | 32 | SET keyname=HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment 33 | FOR /F "usebackq skip=2 tokens=1,2*" %%A IN ( 34 | `REG QUERY "%keyname%" /v "Path" 2^>nul`) DO ( 35 | set "pathname=%%A" 36 | set "pathtype=%%B" 37 | set "pathvalue=%%C" 38 | ) 39 | IF %errorlevel% NEQ 0 EXIT /b %errorlevel% 40 | 41 | REM If not in the system path, then add it there 42 | REM This is difficult: https://stackoverflow.com/a/8046515/604364 43 | REM So we'll only do a simplified check instead 44 | 45 | for /F "delims=" %%L in ( 46 | 'echo ";%pathvalue%;" ^| find /C /I ";%linkdir%;"') do ( 47 | set "cnt=%%L" 48 | ) 49 | IF %errorlevel% NEQ 0 EXIT /b %errorlevel% 50 | 51 | SET newpath=%linkdir%;%pathvalue% 52 | if "%cnt%"=="0" ( 53 | echo Adding '%linkdir%' to the system path. 54 | reg add "%keyname%" /v "Path" /t "%pathtype%" /d "%newpath%" /f >nul 2>nul 55 | IF %errorlevel% NEQ 0 EXIT /b %errorlevel% 56 | ) 57 | 58 | REM SETX will signal an environment refresh, so no reboot is needed 59 | REM We cannot SETX the path, because that would expand the wildcards 60 | 61 | SETX dummy dummy >nul 62 | IF %errorlevel% NEQ 0 EXIT /b %errorlevel% 63 | 64 | REM ---------------------------------------------------------------- 65 | REM Get the R user's home directory, we'll use this to create 66 | REM user package libraries. This is the same lookup that R does, 67 | REM see the R for Windows FAQ, and also ?Rconsole. 68 | REM ---------------------------------------------------------------- 69 | 70 | REM If R_USER is set, we use that 71 | REM It could be set in .Renviron as well? Then we'll miss it here :( 72 | 73 | SET myhome=%R_USER% 74 | 75 | REM Otherwise use HOME, if set. 76 | 77 | IF [%myhome%] == [] ( 78 | SET myhome=%HOME% 79 | ) 80 | 81 | REM Otherwise look up the home in the registry 82 | 83 | IF [%myhome%] == [] ( 84 | SET "homekey=HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" 85 | FOR /F "tokens=3" %%A IN ( 86 | 'REG QUERY "%homekey%" /v "Personal"') DO ( 87 | SET myhome=%%A 88 | ) 89 | ) 90 | 91 | echo Creating user libraries in %myhome%. 92 | 93 | REM ---------------------------------------------------------------- 94 | REM Get the installed R versions and locations from the registry 95 | REM ---------------------------------------------------------------- 96 | 97 | SET rkey=HKEY_LOCAL_MACHINE\SOFTWARE\R-core\R 98 | 99 | REM First we get the pre-release versions. These are added first, 100 | REM so they will be overwritten by the proper releases. 101 | 102 | SET devrversions= 103 | FOR /F "tokens=1 delims=" %%a in ( 104 | 'REG QUERY "%rkey%" /f "*" /k ^| 105 | findstr /v /c:"End of search:" ^| sort ^| findstr /r "Pre-release$" ') DO ( 106 | for /F "tokens=5 delims=\" %%b in ("%%a") do ( 107 | CALL SET "devrversions=%%devrversions%%;%%b" 108 | ) 109 | ) 110 | ) 111 | 112 | REM Then we add all the releases, in the right order, so a later release will 113 | REM overwrite an earlier one, and e.g. R-4.0 will use R 4.0.2, not R 4.0.1. 114 | 115 | SET rversions= 116 | FOR /F "tokens=1 delims=" %%a in ( 117 | 'REG QUERY "%rkey%" /f "*" /k ^| 118 | findstr /v /c:"End of search:" ^| sort ^| findstr /r "[0123456789]$" ') DO ( 119 | for /F "tokens=5 delims=\" %%b in ("%%a") do ( 120 | CALL SET "rversions=%%rversions%%;%%b" 121 | ) 122 | ) 123 | ) 124 | 125 | REM ---------------------------------------------------------------- 126 | REM Create a shortcut for every version 127 | REM ---------------------------------------------------------------- 128 | 129 | REM First we remove all shortcuts, so we can re-populate them. 130 | REM It would be better to remove selectively, in case the script does not 131 | REM run to completion... 132 | 133 | del /f "%linkdir%\R-*.bat" 134 | 135 | REM Need to replace spaces, temporarily, as FOR breaks on them 136 | SET devrversions2=!devrversions: =_! 137 | 138 | for %%a in (%devrversions2%) do ( 139 | for /F "tokens=1,2 delims=." %%b in ("%%a") do call :shcut %%a %%b.%%c 140 | ) 141 | 142 | for %%a in (%rversions%) do ( 143 | for /F "tokens=1,2 delims=." %%b in ("%%a") do call :shcut %%a %%b.%%c 144 | ) 145 | 146 | goto End 147 | 148 | REM ---------------------------------------------------------------- 149 | REM Functions 150 | REM ---------------------------------------------------------------- 151 | 152 | REM Create a shortcut, %1 is the full version number, %2 is the minor 153 | REM version number 154 | 155 | :shcut 156 | SET oldver=%1 157 | SET ver=!oldver:_= ! 158 | SET minor=%2 159 | 160 | FOR /F "usebackq skip=2 tokens=1,2*" %%A IN ( 161 | `REG QUERY "%rkey%\%ver%" /v InstallPath 2^>nul`) DO ( 162 | set "installpath=%%C" 163 | ) 164 | 165 | echo Adding shortcut: %linkdir%\R-%minor%.bat -^> %installpath%\bin\R 166 | echo @"%installpath%\bin\R" %%* > "%linkdir%\R-%minor%.bat" 167 | 168 | echo Adding shortcut: %linkdir%\R-%minor%-i386.bat -^> %installpath%\bin\i386\R 169 | echo @"%installpath%\bin\i386\R" %%* > "%linkdir%\R-%minor%-i386.bat" 170 | 171 | REM Create library directory for this version 172 | REM https://stackoverflow.com/a/4165472/604364 173 | SET "mylibdir=%myhome%\R\win-library\%minor%" 174 | IF NOT EXIST %mylibdir%\NUL ( 175 | mkdir %mylibdir% 176 | ) 177 | 178 | REM Create Renviron.site file, to adjust PATH for Rtools 179 | REM TODO: Rtools directory is hardcoded. 180 | SET "renv=%installpath%\etc\Renviron.site" 181 | IF "%minor:~0,1%" == "3" ( 182 | echo PATH="C:\Rtools\bin;${PATH}" > "%renv%" 183 | ) ELSE ( 184 | echo PATH="${RTOOLS40_HOME}\ucrt64\bin;${RTOOLS40_HOME}\usr\bin;${PATH}" > "%renv%" 185 | ) 186 | 187 | goto :eof 188 | 189 | :End 190 | endlocal 191 | -------------------------------------------------------------------------------- /lib/tempfile.js: -------------------------------------------------------------------------------- 1 | 2 | const temp = require('temp-dir'); 3 | const fs = require('fs'); 4 | const {promisify} = require('util'); 5 | const write_file = promisify(fs.writeFile); 6 | const path = require('path'); 7 | 8 | async function tempfile(prefix = "node-") { 9 | const filename = prefix + Math.random() 10 | .toString(36) 11 | .replace(/[^a-z]+/g, '') 12 | .substr(0, 10); 13 | 14 | const filepath = path.join(temp, filename); 15 | 16 | await write_file(filepath, Buffer.alloc(0)); 17 | return filepath; 18 | } 19 | 20 | module.exports = tempfile; 21 | -------------------------------------------------------------------------------- /lib/url-macos.js: -------------------------------------------------------------------------------- 1 | 2 | const urls = require('./urls'); 3 | const semver = require('semver'); 4 | 5 | function url_macos(version) { 6 | if (version === 'devel') { 7 | return urls.macos_dev; 8 | 9 | } else if (semver.eq(version, '3.2.5')) { 10 | return urls.macos_325; 11 | 12 | } else if (semver.lt(version, "3.4.0")) { 13 | return urls.macos_old2.replace('%s', version); 14 | 15 | } else if (semver.lt(version, "4.0.0")) { 16 | return urls.macos_old.replace('%s', version); 17 | 18 | } else { 19 | return urls.macos.replace('%s', version); 20 | } 21 | } 22 | 23 | module.exports = url_macos; 24 | -------------------------------------------------------------------------------- /lib/url-win.js: -------------------------------------------------------------------------------- 1 | 2 | const resolve_url = require("./resolve-url"); 3 | const urls = require('./urls'); 4 | const semver = require('semver'); 5 | 6 | async function url_win(version) { 7 | if (version === 'devel') { 8 | return urls.win_dev; 9 | } 10 | const rel = await resolve_url("r-release"); 11 | if (semver.eq(rel.version, version)) { 12 | return urls.win.replace(/%s/g, version); 13 | } else { 14 | return urls.win_old.replace(/%s/g, version); 15 | } 16 | } 17 | 18 | module.exports = url_win; 19 | -------------------------------------------------------------------------------- /lib/urls.js: -------------------------------------------------------------------------------- 1 | 2 | urls = { 3 | macos_dev: 'https://files.r-hub.io/macos/R-devel.pkg', 4 | macos: 'https://cloud.r-project.org/bin/macosx/base/R-%s.pkg', 5 | macos_old: 'https://cloud.r-project.org/bin/macosx/R-%s.pkg', 6 | macos_old2: 'https://cloud.r-project.org/bin/macosx/old/R-%s.pkg', 7 | macos_325: 'https://cloud.r-project.org/bin/macosx/old/R-3.2.4-revised.pkg', 8 | win_dev: 'https://cloud.r-project.org/bin/windows/base/R-devel-win.exe', 9 | win: 'https://cran.r-project.org/bin/windows/base/R-%s-win.exe', 10 | win_old: 'https://cloud.r-project.org/bin/windows/base/old/%s/R-%s-win.exe', 11 | rtools: { 12 | '33': 'https://cloud.r-project.org/bin/windows/Rtools/Rtools33.exe', 13 | '35': 'https://cloud.r-project.org/bin/windows/Rtools/Rtools35.exe', 14 | '40': 'https://cloud.r-project.org/bin/windows/Rtools/rtools40-x86_64.exe' 15 | } 16 | } 17 | 18 | module.exports = urls; 19 | -------------------------------------------------------------------------------- /news.md: -------------------------------------------------------------------------------- 1 | 2 | # 1.3.6 3 | 4 | * Fix setting the path on R 4.2.x and Rtools40 on Windows. 5 | 6 | # 1.3.5 7 | 8 | * Fix user library creation on R 4.2.x, on macOS. 9 | 10 | # 1.3.4 11 | 12 | * Create shortcuts for i386 versions on Windows. 13 | 14 | # 1.3.3 15 | 16 | * Fix macOS URLs for R-4.1.0. 17 | 18 | # 1.3.2 19 | 20 | * Update dependencies, in particular, use newer rversions package 21 | to fix some parallel lookup issuies. 22 | 23 | # 1.3.1 24 | 25 | * Do not use sudo on GitHub Actions, on Windows, it gets stuck. 26 | 27 | * Fix updating the path on Windows. 28 | 29 | # 1.3.0 30 | 31 | * Create shortcuts to all installed R versions on Windows as well. 32 | 33 | * Create all user library directories on Windows as well. 34 | 35 | * Automatically install the required Rtools version on Windows, if it is 36 | not installed already, and set it up correctly. 37 | 38 | # 1.2.1 39 | 40 | * Fix macOS R-devel installation, we now use the installer from R-hub. 41 | 42 | * Fix a potential crash, when using install-rstats after a manual 43 | installation. 44 | 45 | # 1.2.0 46 | 47 | * On macOS, `install-rstats` now does not ask for the password, if the 48 | user can run `sudo` without one. 49 | 50 | # 1.1.0 51 | 52 | * install-rstats now supports Windows as well. 53 | 54 | * The installer now creates all user library directories as well. 55 | 56 | # 1.0.1 57 | 58 | * For robustness, now we use the https://api.r-hub.io/rversions web 59 | service to resolve R versions, with a fallback to the R servers. 60 | 61 | * Download now does not fail if the base name of the URL is not a 62 | valid filename. Invalid characters are converted to a dash. 63 | 64 | * Download now creates the temporary directory if it does not exist. 65 | 66 | # 1.0.0 67 | 68 | First published release. 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "install-rstats", 3 | "version": "1.3.6", 4 | "description": "Install R versions", 5 | "main": "index.js", 6 | "repository": "r-hub/node-install-rstats", 7 | "scripts": { 8 | "test": "nyc ava" 9 | }, 10 | "keywords": [ 11 | "install", 12 | "R" 13 | ], 14 | "author": "Gábor Csárdi", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "ava": "^3.14.0", 18 | "create-test-server": "^3.0.1", 19 | "nyc": "^15.1.0" 20 | }, 21 | "dependencies": { 22 | "array-unique": "^0.3.2", 23 | "bluebird": "^3.7.2", 24 | "execa": "^4.1.0", 25 | "filenamify": "^4.2.0", 26 | "got": "^11.8.1", 27 | "meow": "^7.1.1", 28 | "mkdirp": "^1.0.4", 29 | "ora": "^4.1.1", 30 | "pretty-bytes": "^5.4.1", 31 | "rversions": "^1.2.0", 32 | "semver": "^7.3.4", 33 | "sudo-prompt": "^9.2.1", 34 | "tail": "^2.0.4", 35 | "temp-dir": "^2.0.0", 36 | "winreg": "^1.2.4" 37 | }, 38 | "bin": { 39 | "install-rstats": "install-rstats.js" 40 | }, 41 | "files": [ 42 | "index.js", 43 | "install-rstats.js", 44 | "news.md", 45 | "readme.md", 46 | "package.json", 47 | "lib" 48 | ], 49 | "pkg": { 50 | "build": [ 51 | "install-rstats.js", 52 | "lib/installer.sh" 53 | ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | # This project is now archived in favor of https://github.com/r-lib/rig 3 | 4 | # install-rstats 5 | 6 | > Install various versions of R on macOS and Windows 7 | 8 | ## Features 9 | 10 | * Downloads and installs multiple R versions, with a single command. 11 | * Supports symbolic version names: `release`, `devel` and `oldrel`. 12 | * On Windows, it installs the correct Rtools versions (if they are not 13 | installed already), and sets them up. 14 | * Patches R to allow running multiple R versions at the same time. 15 | (macOS, not needed for Windows) 16 | * Adds symlinks/shortcuts to start a certain R version: e.g. `R-4.0`. 17 | * Updates access rights of to forbid installing packages into the 18 | system R library. (macOS, not needed for Windows) 19 | * Creates user package libraries. 20 | 21 | ## Install 22 | 23 | ``` 24 | $ npm install -g install-rstats 25 | ``` 26 | 27 | ## Usage 28 | 29 | ### Command line 30 | 31 | ```sh 32 | install-rstats [rversion1] [rversion2] ... 33 | ``` 34 | 35 | On macOS, if you start `install-rstats` wihout `sudo`, it will ask for 36 | your password (once) in a dialog box. If you want to avoid that, use 37 | `sudo` at the line: 38 | 39 | ```sh 40 | sudo install-rstats [rversion1] [rversion2] ... 41 | ``` 42 | 43 | ### From node.js 44 | 45 | Use the `install()` function and supply the desired R versions in an 46 | array. If no version is given, it installs the latest R release. 47 | 48 | Example: 49 | 50 | ```js 51 | const installr = require('install-rstats'); 52 | 53 | (async () => { 54 | await installr.install(['3.6', '4.0', 'devel']) 55 | })(); 56 | ``` 57 | 58 | ### Supported R versions 59 | 60 | * `release` is the current R release, 61 | * `oldrel` is the latest release of the previous minor branch, 62 | * `devel` is a development snapshot, 63 | * `x.y` is the latest release of the x.y branch, 64 | * `x.y.z` is version x.y.z. 65 | 66 | ## License 67 | 68 | ISC @ R Consortium 69 | 70 | This repo is part of the R-hub project, supported by 71 | the R Consortium. 72 | -------------------------------------------------------------------------------- /tests/test-download-files.js: -------------------------------------------------------------------------------- 1 | 2 | const create_test_server = require('create-test-server'); 3 | const server = create_test_server(); 4 | const test = require('ava'); 5 | 6 | const download_files = require('../lib/download-files'); 7 | 8 | const {promisify} = require('util'); 9 | const fs = require('fs'); 10 | const path = require('path'); 11 | const unlink = promisify(fs.unlink); 12 | const access = promisify(fs.access); 13 | const readFile = promisify(fs.readFile); 14 | 15 | async function unlink_try(path) { 16 | try { 17 | await unlink(path); 18 | } catch(err) { 19 | // nothing 20 | } 21 | } 22 | 23 | test('download_files', async t => { 24 | 25 | const srv = await server; 26 | srv.get("/download/:cnt", function(req, res) { 27 | res.type("text/plain") 28 | .send(req.params.cnt) 29 | }); 30 | srv.get("/download-fail/:cnt", function(req, res) { 31 | res.sendStatus(404); 32 | }); 33 | 34 | const urls = [ srv.url + '/download/foo', 35 | srv.url + '/download/bar', 36 | srv.url + '/download/foobar' ]; 37 | 38 | const filenames = await download_files(urls); 39 | await Promise.all([ unlink_try(filenames[0]), 40 | unlink_try(filenames[1])]); 41 | await download_files(urls); 42 | 43 | t.is(path.basename(filenames[0]), 'foo'); 44 | t.is(path.basename(filenames[1]), 'bar'); 45 | t.is(path.basename(filenames[2]), 'foobar'); 46 | 47 | t.notThrows(async () => await access(filenames[0])); 48 | t.notThrows(async () => await access(filenames[1])); 49 | t.notThrows(async () => await access(filenames[2])); 50 | 51 | var cnt = await readFile(filenames[0], 'utf8'); 52 | t.is(cnt, 'foo'); 53 | var cnt = await readFile(filenames[1], 'utf8'); 54 | t.is(cnt, 'bar'); 55 | var cnt = await readFile(filenames[2], 'utf8'); 56 | t.is(cnt, 'foobar'); 57 | 58 | await Promise.all([ unlink_try(filenames[0]), 59 | unlink_try(filenames[1]), 60 | unlink_try(filenames[2]) ]); 61 | 62 | const urls2 = [ srv.url + '/download/foo', 63 | srv.url + '/download-fail/bar', 64 | srv.url + '/download/foobar' ]; 65 | 66 | await t.throwsAsync(async() => { return await download_files(urls2) }); 67 | 68 | await Promise.all([unlink_try(filenames[0]), 69 | unlink_try(filenames[1]), 70 | unlink_try(filenames[2]) ]); 71 | }); 72 | -------------------------------------------------------------------------------- /tests/test-download-maybe.js: -------------------------------------------------------------------------------- 1 | 2 | const create_test_server = require('create-test-server'); 3 | const server = create_test_server(); 4 | const test = require('ava'); 5 | 6 | const download_maybe = require('../lib/download-maybe'); 7 | 8 | const fs = require('fs'); 9 | const path = require('path'); 10 | 11 | const {promisify} = require('util'); 12 | const access = promisify(fs.access); 13 | const writeFile = promisify(fs.writeFile); 14 | const readFile = promisify(fs.readFile); 15 | const unlink = promisify(fs.unlink); 16 | 17 | test('download_maybe', async t => { 18 | 19 | const srv = await server; 20 | srv.get("/get", function(req, res) { 21 | res.send(req.query); 22 | }); 23 | 24 | // Will download to temp file. We download twice, first to get the 25 | // file name, then we remove the file and download it for sure. 26 | const url = srv.url + '/get?' + process.pid; 27 | const filename = await download_maybe(url); 28 | await unlink(filename); 29 | await download_maybe(url); 30 | t.is(path.basename(filename), 'get-' + process.pid); 31 | t.notThrows(async () => await access(filename)); 32 | 33 | await writeFile(filename, 'foobar'); 34 | await download_maybe(url); 35 | const cnt = await readFile(filename, 'utf8'); 36 | t.is(cnt, 'foobar'); 37 | 38 | await unlink(filename); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/test-macos.js: -------------------------------------------------------------------------------- 1 | 2 | const test = require('ava'); 3 | const me = require('..'); 4 | 5 | test('install', async t => { 6 | t.pass(); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/test-resolve-all-versions.js: -------------------------------------------------------------------------------- 1 | 2 | const create_test_server = require('create-test-server'); 3 | const server = create_test_server(); 4 | 5 | const test = require('ava'); 6 | 7 | test('resolve_all_versions', async t => { 8 | 9 | const srv = await server; 10 | srv.get( 11 | ['/r-release', '/r-release-macos', '/r-release-win'], 12 | function(req, res) { res.send({ version: '4.0.0'}); } 13 | ); 14 | srv.get('/r-oldrel', function(req, res) { 15 | res.send({ version: '3.6.3' }); 16 | }); 17 | srv.get('/r-versions', function(req, res) { 18 | res.send([ 19 | { version: '3.2.4' }, 20 | { version: '3.2.5' }, 21 | { version: '3.3.2' }, 22 | { version: '3.3.3' }, 23 | { version: '3.6.2' } 24 | ]); 25 | }); 26 | process.env.R_VERSIONS_API_URL = srv.url + '/'; 27 | 28 | const resolve_all_versions = require('../lib/resolve-all-versions'); 29 | 30 | const vers = await resolve_all_versions([ 'devel', '3.2', 'release']); 31 | t.deepEqual(vers, [ 'devel', '3.2.5', '4.0.0' ]); 32 | 33 | await t.throwsAsync( 34 | async() => { await resolve_all_versions(['foo']) } 35 | ); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/test-resolve-url.js: -------------------------------------------------------------------------------- 1 | 2 | const create_test_server = require('create-test-server'); 3 | const server = create_test_server(); 4 | 5 | const test = require('ava'); 6 | 7 | test('resolve_url fallback', async t => { 8 | 9 | const srv = await server; 10 | srv.get('/404', function(req, res) { 11 | res.sendStatus(404); 12 | }); 13 | process.env.R_VERSIONS_API_URL = srv.url + '/'; 14 | process.env.NODE_RVERSIONS_DUMMY = 'true'; 15 | 16 | const resolve_url = require('../lib/resolve-url'); 17 | 18 | const rrel = await resolve_url('r-release'); 19 | t.true(!!rrel.version); 20 | 21 | // Unknown query 22 | await t.throwsAsync(resolve_url('foobar')); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/test-tempfile.js: -------------------------------------------------------------------------------- 1 | 2 | const test = require('ava'); 3 | const tempfile = require('../lib/tempfile'); 4 | 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | 8 | const {promisify} = require('util'); 9 | const access = promisify(fs.access); 10 | const readFile = promisify(fs.readFile); 11 | const unlink = promisify(fs.unlink); 12 | 13 | test('tempfile', async t => { 14 | const filename = await tempfile('prefix-'); 15 | t.notThrows(async () => await access(filename)); 16 | t.is(path.basename(filename).slice(0, 7), 'prefix-'); 17 | t.true(path.basename(filename).length > 10); 18 | const cnt = await readFile(filename, 'utf8'); 19 | t.is(cnt, ''); 20 | await unlink(filename); 21 | 22 | // Default prefix 23 | const filename2 = await tempfile(); 24 | t.is(path.basename(filename2).slice(0, 5), 'node-'); 25 | await unlink(filename2); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/test-versions.js: -------------------------------------------------------------------------------- 1 | 2 | const create_test_server = require('create-test-server'); 3 | const server = create_test_server(); 4 | 5 | const test = require('ava'); 6 | const url_macos = require('../lib/url-macos'); 7 | 8 | const semver = require('semver'); 9 | 10 | test('resolve_version', async t => { 11 | 12 | const srv = await server; 13 | srv.get( 14 | ['/r-release', '/r-release-macos', '/r-release-win'], 15 | function(req, res) { res.send({ version: '4.0.0'}); } 16 | ); 17 | srv.get('/r-oldrel', function(req, res) { 18 | res.send({ version: '3.6.3' }); 19 | }); 20 | srv.get('/r-versions', function(req, res) { 21 | res.send([ 22 | { version: '3.2.4' }, 23 | { version: '3.2.5' }, 24 | { version: '3.3.2' }, 25 | { version: '3.3.3' }, 26 | { version: '3.6.2' } 27 | ]); 28 | }); 29 | process.env.R_VERSIONS_API_URL = srv.url + '/'; 30 | 31 | const resolve_version = require('../lib/resolve-version'); 32 | 33 | // 'devel' is 'devel' 34 | t.is(await resolve_version('devel'), 'devel'); 35 | 36 | // Version number is checked and returned 37 | const r362 = await resolve_version('3.6.2'); 38 | t.is(r362, '3.6.2'); 39 | 40 | // Default is release 41 | const rdef = await resolve_version(); 42 | const rdef2 = await resolve_version('release'); 43 | t.is(rdef, rdef2); 44 | 45 | // Errors if unknown 46 | const err = await t.throwsAsync(resolve_version('0.0.1')); 47 | t.is(err.message, 'Unknown R version: 0.0.1'); 48 | 49 | // Errors if unknown 50 | const err2 = await t.throwsAsync(resolve_version('0.1')); 51 | t.is(err2.message, 'Unknown minor R version: 0.1'); 52 | 53 | // Errors if unknown OS 54 | const err3 = await t.throwsAsync(resolve_version('release', 'nix')); 55 | t.is(err3.message, 'Unknown OS in `resolve_version()`: nix'); 56 | 57 | // Resolves 'release' correctly 58 | const rls = await resolve_version('release'); 59 | t.regex(rls, /^[0-9]+\.[0-9]+\.[0-9]+$/); 60 | 61 | // Resolves 'oldrel' as well 62 | const old = await resolve_version('oldrel'); 63 | t.regex(old, /^[0-9]+\.[0-9]+\.[0-9]+$/); 64 | t.true(semver.lt(old, rls)); 65 | 66 | // Resolves minor versions properly 67 | const r325 = await resolve_version('3.2'); 68 | t.is(r325, '3.2.5'); 69 | const r333 = await resolve_version('3.3'); 70 | t.is(r333, '3.3.3'); 71 | 72 | // Resolves OS specific versions 73 | const mac = await resolve_version('release', 'mac'); 74 | t.regex(mac, /^[0-9]+\.[0-9]+\.[0-9]+$/); 75 | 76 | const win = await resolve_version('release', 'win'); 77 | t.regex(win, /^[0-9]+\.[0-9]+\.[0-9]+$/); 78 | }); 79 | 80 | test('url_macos', t => { 81 | t.regex( 82 | url_macos('devel'), 83 | /^https:\/\// 84 | ) 85 | t.is( 86 | url_macos('3.2.5'), 87 | 'https://cloud.r-project.org/bin/macosx/old/R-3.2.4-revised.pkg' 88 | ) 89 | t.is( 90 | url_macos('3.6.2'), 91 | 'https://cloud.r-project.org/bin/macosx/R-3.6.2.pkg' 92 | ) 93 | t.is( 94 | url_macos('3.3.0'), 95 | 'https://cloud.r-project.org/bin/macosx/old/R-3.3.0.pkg' 96 | ) 97 | }); 98 | --------------------------------------------------------------------------------