diff --git a/.gitignore b/.gitignore index 31ed630..fb017f4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ *~ -tmp \ No newline at end of file +tmp +target/ +**/*.rs.bk +Cargo.lock diff --git a/.travis.yml b/.travis.yml index 64a9a9a..bcc3999 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,4 @@ -addons: - apt: - packages: - - p7zip-full - +language: rust script: + - cargo build - ./test.sh diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f998caa --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,25 @@ +[package] +authors = ["The Rust Project Developers"] +name = "installer" +version = "0.0.0" + +[[bin]] +doc = false +name = "rust-installer" +path = "src/main.rs" + +[dependencies] +error-chain = "0.10.0" +flate2 = "0.2.19" +tar = "0.4.11" +walkdir = "1.0.7" +xz2 = "0.1.3" + +[dependencies.clap] +features = ["yaml"] +version = "2.22.1" + +[target."cfg(windows)".dependencies] +lazy_static = "0.2.8" +kernel32-sys = "0.2.2" +winapi = "0.2.8" diff --git a/combine-installers.sh b/combine-installers.sh index 76d10a7..e56dc8d 100755 --- a/combine-installers.sh +++ b/combine-installers.sh @@ -9,194 +9,7 @@ # option. This file may not be copied, modified, or distributed # except according to those terms. -set -u - -msg() { - echo "combine-installers: ${1-}" -} - -step_msg() { - msg - msg "$1" - msg -} - -warn() { - echo "combine-installers: WARNING: $1" >&2 -} - -err() { - echo "combine-installers: error: $1" >&2 - exit 1 -} - -need_ok() { - if [ $? -ne 0 ] - then - err "$1" - fi -} - -need_cmd() { - if command -v $1 >/dev/null 2>&1 - then msg "found $1" - else err "need $1" - fi -} - -putvar() { - local t - local tlen - eval t=\$$1 - eval tlen=\${#$1} - if [ $tlen -gt 35 ] - then - printf "combine-installers: %-20s := %.35s ...\n" $1 "$t" - else - printf "combine-installers: %-20s := %s %s\n" $1 "$t" - fi -} - -valopt() { - VAL_OPTIONS="$VAL_OPTIONS $1" - - local op=$1 - local default=$2 - shift - shift - local doc="$*" - if [ $HELP -eq 0 ] - then - local uop=$(echo $op | tr '[:lower:]' '[:upper:]' | tr '\-' '\_') - local v="CFG_${uop}" - eval $v="$default" - for arg in $CFG_ARGS - do - if echo "$arg" | grep -q -- "--$op=" - then - local val=$(echo "$arg" | cut -f2 -d=) - eval $v=$val - fi - done - putvar $v - else - if [ -z "$default" ] - then - default="" - fi - op="${default}=[${default}]" - printf " --%-30s %s\n" "$op" "$doc" - fi -} - -opt() { - BOOL_OPTIONS="$BOOL_OPTIONS $1" - - local op=$1 - local default=$2 - shift - shift - local doc="$*" - local flag="" - - if [ $default -eq 0 ] - then - flag="enable" - else - flag="disable" - doc="don't $doc" - fi - - if [ $HELP -eq 0 ] - then - for arg in $CFG_ARGS - do - if [ "$arg" = "--${flag}-${op}" ] - then - op=$(echo $op | tr 'a-z-' 'A-Z_') - flag=$(echo $flag | tr 'a-z' 'A-Z') - local v="CFG_${flag}_${op}" - eval $v=1 - putvar $v - fi - done - else - if [ ! -z "$META" ] - then - op="$op=<$META>" - fi - printf " --%-30s %s\n" "$flag-$op" "$doc" - fi -} - -flag() { - BOOL_OPTIONS="$BOOL_OPTIONS $1" - - local op=$1 - shift - local doc="$*" - - if [ $HELP -eq 0 ] - then - for arg in $CFG_ARGS - do - if [ "$arg" = "--${op}" ] - then - op=$(echo $op | tr 'a-z-' 'A-Z_') - local v="CFG_${op}" - eval $v=1 - putvar $v - fi - done - else - if [ ! -z "$META" ] - then - op="$op=<$META>" - fi - printf " --%-30s %s\n" "$op" "$doc" - fi -} - -validate_opt () { - for arg in $CFG_ARGS - do - local is_arg_valid=0 - for option in $BOOL_OPTIONS - do - if test --disable-$option = $arg - then - is_arg_valid=1 - fi - if test --enable-$option = $arg - then - is_arg_valid=1 - fi - if test --$option = $arg - then - is_arg_valid=1 - fi - done - for option in $VAL_OPTIONS - do - if echo "$arg" | grep -q -- "--$option=" - then - is_arg_valid=1 - fi - done - if [ "$arg" = "--help" ] - then - echo - echo "No more help available for Configure options," - echo "check the Wiki or join our IRC channel" - break - else - if test $is_arg_valid -eq 0 - then - err "Option '$arg' is not recognized" - fi - fi - done -} +set -ue # Prints the absolute path of a directory to stdout abs_path() { @@ -207,128 +20,5 @@ abs_path() { (unset CDPATH && cd "$path" > /dev/null && pwd) } -msg "looking for programs" -msg - -need_cmd tar -need_cmd cp -need_cmd rm -need_cmd mkdir -need_cmd echo -need_cmd tr - -CFG_ARGS="$@" - -HELP=0 -if [ "$1" = "--help" ] -then - HELP=1 - shift - echo - echo "Usage: $0 [options]" - echo - echo "Options:" - echo -else - step_msg "processing arguments" -fi - -OPTIONS="" -BOOL_OPTIONS="" -VAL_OPTIONS="" - -valopt product-name "Product" "The name of the product, for display" -valopt package-name "package" "The name of the package, tarball" -valopt rel-manifest-dir "${CFG_PACKAGE_NAME}lib" "The directory under lib/ where the manifest lives" -valopt success-message "Installed." "The string to print after successful installation" -valopt legacy-manifest-dirs "" "Places to look for legacy manifests to uninstall" -valopt input-tarballs "" "Installers to combine" -valopt non-installed-overlay "" "Directory containing files that should not be installed" -valopt work-dir "./workdir" "The directory to do temporary work and put the final image" -valopt output-dir "./dist" "The location to put the final tarball" - -if [ $HELP -eq 1 ] -then - echo - exit 0 -fi - -step_msg "validating arguments" -validate_opt - src_dir="$(abs_path $(dirname "$0"))" - -rust_installer_version=`cat "$src_dir/rust-installer-version"` - -# Create the work directory for the new installer -mkdir -p "$CFG_WORK_DIR" -need_ok "couldn't create work dir" - -rm -Rf "$CFG_WORK_DIR/$CFG_PACKAGE_NAME" -need_ok "couldn't delete work package dir" - -mkdir -p "$CFG_WORK_DIR/$CFG_PACKAGE_NAME" -need_ok "couldn't create work package dir" - -input_tarballs=`echo "$CFG_INPUT_TARBALLS" | sed 's/,/ /g'` - -# Merge each installer into the work directory of the new installer -for input_tarball in $input_tarballs; do - - # Extract the input tarballs - tar xzf $input_tarball -C "$CFG_WORK_DIR" - need_ok "failed to extract tarball" - - # Verify the version number - pkg_name=`echo "$input_tarball" | sed s/\.tar\.gz//g` - pkg_name=`basename $pkg_name` - version=`cat "$CFG_WORK_DIR/$pkg_name/rust-installer-version"` - if [ "$rust_installer_version" != "$version" ]; then - err "incorrect installer version in $input_tarball" - fi - - # Copy components to new combined installer - components=`cat "$CFG_WORK_DIR/$pkg_name/components"` - for component in $components; do - - # All we need to do is copy the component directory - cp -R "$CFG_WORK_DIR/$pkg_name/$component" "$CFG_WORK_DIR/$CFG_PACKAGE_NAME/$component" - need_ok "failed to copy component $component" - - # Merge the component name - echo "$component" >> "$CFG_WORK_DIR/$CFG_PACKAGE_NAME/components" - need_ok "failed to merge component $component" - done -done - -# Write the version number -echo "$rust_installer_version" > "$CFG_WORK_DIR/$CFG_PACKAGE_NAME/rust-installer-version" - -# Copy the overlay -if [ -n "$CFG_NON_INSTALLED_OVERLAY" ]; then - overlay_files=`(cd "$CFG_NON_INSTALLED_OVERLAY" && find . -type f)` - for f in $overlay_files; do - if [ -e "$CFG_WORK_DIR/$CFG_PACKAGE_NAME/$f" ]; then err "overlay $f exists"; fi - - cp "$CFG_NON_INSTALLED_OVERLAY/$f" "$CFG_WORK_DIR/$CFG_PACKAGE_NAME/$f" - need_ok "failed to copy overlay $f" - done -fi - -# Generate the install script -"$src_dir/gen-install-script.sh" \ - --product-name="$CFG_PRODUCT_NAME" \ - --rel-manifest-dir="$CFG_REL_MANIFEST_DIR" \ - --success-message="$CFG_SUCCESS_MESSAGE" \ - --legacy-manifest-dirs="$CFG_LEGACY_MANIFEST_DIRS" \ - --output-script="$CFG_WORK_DIR/$CFG_PACKAGE_NAME/install.sh" - -need_ok "failed to generate install script" - -mkdir -p "$CFG_OUTPUT_DIR" -need_ok "couldn't create output dir" - -"$src_dir/make-tarballs.sh" \ - --work-dir="$CFG_WORK_DIR" \ - --input="$CFG_PACKAGE_NAME" \ - --output="$CFG_OUTPUT_DIR/$CFG_PACKAGE_NAME" +cargo run --manifest-path="$src_dir/Cargo.toml" -- combine "$@" diff --git a/gen-install-script.sh b/gen-install-script.sh index 620fcf7..1420814 100755 --- a/gen-install-script.sh +++ b/gen-install-script.sh @@ -9,200 +9,7 @@ # option. This file may not be copied, modified, or distributed # except according to those terms. -set -u - -if [ -x /bin/echo ]; then - ECHO='/bin/echo' -else - ECHO='echo' -fi - -msg() { - echo "gen-install-script: ${1-}" -} - -step_msg() { - msg - msg "$1" - msg -} - -warn() { - echo "gen-install-script: WARNING: $1" >&2 -} - -err() { - echo "gen-install-script: error: $1" >&2 - exit 1 -} - -need_ok() { - if [ $? -ne 0 ] - then - err "$1" - fi -} - -need_cmd() { - if command -v $1 >/dev/null 2>&1 - then msg "found $1" - else err "need $1" - fi -} - -putvar() { - local t - local tlen - eval t=\$$1 - eval tlen=\${#$1} - if [ $tlen -gt 35 ] - then - printf "gen-install-script: %-20s := %.35s ...\n" $1 "$t" - else - printf "gen-install-script: %-20s := %s %s\n" $1 "$t" - fi -} - -valopt() { - VAL_OPTIONS="$VAL_OPTIONS $1" - - local op=$1 - local default=$2 - shift - shift - local doc="$*" - if [ $HELP -eq 0 ] - then - local uop=$(echo $op | tr '[:lower:]' '[:upper:]' | tr '\-' '\_') - local v="CFG_${uop}" - eval $v="$default" - for arg in $CFG_ARGS - do - if echo "$arg" | grep -q -- "--$op=" - then - local val=$(echo "$arg" | cut -f2 -d=) - eval $v=$val - fi - done - putvar $v - else - if [ -z "$default" ] - then - default="" - fi - op="${default}=[${default}]" - printf " --%-30s %s\n" "$op" "$doc" - fi -} - -opt() { - BOOL_OPTIONS="$BOOL_OPTIONS $1" - - local op=$1 - local default=$2 - shift - shift - local doc="$*" - local flag="" - - if [ $default -eq 0 ] - then - flag="enable" - else - flag="disable" - doc="don't $doc" - fi - - if [ $HELP -eq 0 ] - then - for arg in $CFG_ARGS - do - if [ "$arg" = "--${flag}-${op}" ] - then - op=$(echo $op | tr 'a-z-' 'A-Z_') - flag=$(echo $flag | tr 'a-z' 'A-Z') - local v="CFG_${flag}_${op}" - eval $v=1 - putvar $v - fi - done - else - if [ ! -z "$META" ] - then - op="$op=<$META>" - fi - printf " --%-30s %s\n" "$flag-$op" "$doc" - fi -} - -flag() { - BOOL_OPTIONS="$BOOL_OPTIONS $1" - - local op=$1 - shift - local doc="$*" - - if [ $HELP -eq 0 ] - then - for arg in $CFG_ARGS - do - if [ "$arg" = "--${op}" ] - then - op=$(echo $op | tr 'a-z-' 'A-Z_') - local v="CFG_${op}" - eval $v=1 - putvar $v - fi - done - else - if [ ! -z "$META" ] - then - op="$op=<$META>" - fi - printf " --%-30s %s\n" "$op" "$doc" - fi -} - -validate_opt () { - for arg in $CFG_ARGS - do - local is_arg_valid=0 - for option in $BOOL_OPTIONS - do - if test --disable-$option = $arg - then - is_arg_valid=1 - fi - if test --enable-$option = $arg - then - is_arg_valid=1 - fi - if test --$option = $arg - then - is_arg_valid=1 - fi - done - for option in $VAL_OPTIONS - do - if echo "$arg" | grep -q -- "--$option=" - then - is_arg_valid=1 - fi - done - if [ "$arg" = "--help" ] - then - echo - echo "No more help available for Configure options," - echo "check the Wiki or join our IRC channel" - break - else - if test $is_arg_valid -eq 0 - then - err "Option '$arg' is not recognized" - fi - fi - done -} +set -ue # Prints the absolute path of a directory to stdout abs_path() { @@ -213,69 +20,5 @@ abs_path() { (unset CDPATH && cd "$path" > /dev/null && pwd) } -msg "looking for install programs" -msg - -need_cmd sed -need_cmd chmod -need_cmd cat - -CFG_ARGS="$@" - -HELP=0 -if [ "$1" = "--help" ] -then - HELP=1 - shift - echo - echo "Usage: $0 [options]" - echo - echo "Options:" - echo -else - step_msg "processing arguments" -fi - -OPTIONS="" -BOOL_OPTIONS="" -VAL_OPTIONS="" - -valopt product-name "Product" "The name of the product, for display" -valopt rel-manifest-dir "manifestlib" "The directory under lib/ where the manifest lives" -valopt success-message "Installed." "The string to print after successful installation" -valopt output-script "install.sh" "The name of the output script" -valopt legacy-manifest-dirs "" "Places to look for legacy manifests to uninstall" - -if [ $HELP -eq 1 ] -then - echo - exit 0 -fi - -step_msg "validating arguments" -validate_opt - src_dir="$(abs_path $(dirname "$0"))" - -rust_installer_version=`cat "$src_dir/rust-installer-version"` - -# Replace dashes in the success message with spaces (our arg handling botches spaces) -product_name=`echo "$CFG_PRODUCT_NAME" | sed "s/-/ /g"` - -# Replace dashes in the success message with spaces (our arg handling botches spaces) -success_message=`echo "$CFG_SUCCESS_MESSAGE" | sed "s/-/ /g"` - -script_template=`cat "$src_dir/install-template.sh"` - -# Using /bin/echo because under sh emulation dash *seems* to escape \n, which screws up the template -script=`$ECHO "$script_template"` -script=`$ECHO "$script" | sed "s/%%TEMPLATE_PRODUCT_NAME%%/\"$product_name\"/"` -script=`$ECHO "$script" | sed "s/%%TEMPLATE_REL_MANIFEST_DIR%%/$CFG_REL_MANIFEST_DIR/"` -script=`$ECHO "$script" | sed "s/%%TEMPLATE_SUCCESS_MESSAGE%%/\"$success_message\"/"` -script=`$ECHO "$script" | sed "s/%%TEMPLATE_LEGACY_MANIFEST_DIRS%%/\"$CFG_LEGACY_MANIFEST_DIRS\"/"` -script=`$ECHO "$script" | sed "s/%%TEMPLATE_RUST_INSTALLER_VERSION%%/\"$rust_installer_version\"/"` - -$ECHO "$script" > "$CFG_OUTPUT_SCRIPT" -need_ok "couldn't write script" -chmod u+x "$CFG_OUTPUT_SCRIPT" -need_ok "couldn't chmod script" +cargo run --manifest-path="$src_dir/Cargo.toml" -- script "$@" diff --git a/gen-installer.sh b/gen-installer.sh index a85f1aa..60fac3b 100755 --- a/gen-installer.sh +++ b/gen-installer.sh @@ -11,193 +11,6 @@ set -ue -msg() { - echo "gen-installer: ${1-}" -} - -step_msg() { - msg - msg "$1" - msg -} - -warn() { - echo "gen-installer: WARNING: $1" >&2 -} - -err() { - echo "gen-installer: error: $1" >&2 - exit 1 -} - -need_ok() { - if [ $? -ne 0 ] - then - err "$1" - fi -} - -need_cmd() { - if command -v $1 >/dev/null 2>&1 - then msg "found $1" - else err "need $1" - fi -} - -putvar() { - local t - local tlen - eval t=\$$1 - eval tlen=\${#$1} - if [ $tlen -gt 35 ] - then - printf "gen-installer: %-20s := %.35s ...\n" $1 "$t" - else - printf "gen-installer: %-20s := %s %s\n" $1 "$t" - fi -} - -valopt() { - VAL_OPTIONS="$VAL_OPTIONS $1" - - local op=$1 - local default=$2 - shift - shift - local doc="$*" - if [ $HELP -eq 0 ] - then - local uop=$(echo $op | tr '[:lower:]' '[:upper:]' | tr '\-' '\_') - local v="CFG_${uop}" - eval $v="$default" - for arg in $CFG_ARGS - do - if echo "$arg" | grep -q -- "--$op=" - then - local val=$(echo "$arg" | cut -f2 -d=) - eval $v=$val - fi - done - putvar $v - else - if [ -z "$default" ] - then - default="" - fi - op="${default}=[${default}]" - printf " --%-30s %s\n" "$op" "$doc" - fi -} - -opt() { - BOOL_OPTIONS="$BOOL_OPTIONS $1" - - local op=$1 - local default=$2 - shift - shift - local doc="$*" - local flag="" - - if [ $default -eq 0 ] - then - flag="enable" - else - flag="disable" - doc="don't $doc" - fi - - if [ $HELP -eq 0 ] - then - for arg in $CFG_ARGS - do - if [ "$arg" = "--${flag}-${op}" ] - then - op=$(echo $op | tr 'a-z-' 'A-Z_') - flag=$(echo $flag | tr 'a-z' 'A-Z') - local v="CFG_${flag}_${op}" - eval $v=1 - putvar $v - fi - done - else - if [ ! -z "$META" ] - then - op="$op=<$META>" - fi - printf " --%-30s %s\n" "$flag-$op" "$doc" - fi -} - -flag() { - BOOL_OPTIONS="$BOOL_OPTIONS $1" - - local op=$1 - shift - local doc="$*" - - if [ $HELP -eq 0 ] - then - for arg in $CFG_ARGS - do - if [ "$arg" = "--${op}" ] - then - op=$(echo $op | tr 'a-z-' 'A-Z_') - local v="CFG_${op}" - eval $v=1 - putvar $v - fi - done - else - if [ ! -z "$META" ] - then - op="$op=<$META>" - fi - printf " --%-30s %s\n" "$op" "$doc" - fi -} - -validate_opt () { - for arg in $CFG_ARGS - do - local is_arg_valid=0 - for option in $BOOL_OPTIONS - do - if test --disable-$option = $arg - then - is_arg_valid=1 - fi - if test --enable-$option = $arg - then - is_arg_valid=1 - fi - if test --$option = $arg - then - is_arg_valid=1 - fi - done - for option in $VAL_OPTIONS - do - if echo "$arg" | grep -q -- "--$option=" - then - is_arg_valid=1 - fi - done - if [ "$arg" = "--help" ] - then - echo - echo "No more help available for Configure options," - echo "check the Wiki or join our IRC channel" - break - else - if test $is_arg_valid -eq 0 - then - err "Option '$arg' is not recognized" - fi - fi - done -} - # Prints the absolute path of a directory to stdout abs_path() { local path="$1" @@ -207,138 +20,5 @@ abs_path() { (unset CDPATH && cd "$path" > /dev/null && pwd) } -msg "looking for programs" -msg - -need_cmd cp -need_cmd rm -need_cmd mkdir -need_cmd echo -need_cmd tr -need_cmd awk - -CFG_ARGS="$@" - -HELP=0 -if [ "$1" = "--help" ] -then - HELP=1 - shift - echo - echo "Usage: $0 [options]" - echo - echo "Options:" - echo -else - step_msg "processing arguments" -fi - -OPTIONS="" -BOOL_OPTIONS="" -VAL_OPTIONS="" - -valopt product-name "Product" "The name of the product, for display" -valopt component-name "component" "The name of the component, distinct from other installed components" -valopt package-name "package" "The name of the package, tarball" -valopt rel-manifest-dir "${CFG_PACKAGE_NAME}lib" "The directory under lib/ where the manifest lives" -valopt success-message "Installed." "The string to print after successful installation" -valopt legacy-manifest-dirs "" "Places to look for legacy manifests to uninstall" -valopt non-installed-overlay "" "Directory containing files that should not be installed" -valopt bulk-dirs "" "Path prefixes of directories that should be installed/uninstalled in bulk" -valopt image-dir "./install-image" "The directory containing the installation medium" -valopt work-dir "./workdir" "The directory to do temporary work" -valopt output-dir "./dist" "The location to put the final image and tarball" - -if [ $HELP -eq 1 ] -then - echo - exit 0 -fi - -step_msg "validating arguments" -validate_opt - src_dir="$(abs_path $(dirname "$0"))" - -rust_installer_version=`cat "$src_dir/rust-installer-version"` - -if [ ! -d "$CFG_IMAGE_DIR" ] -then - err "image dir $CFG_IMAGE_DIR does not exist" -fi - -mkdir -p "$CFG_WORK_DIR" -need_ok "couldn't create work dir" - -rm -Rf "$CFG_WORK_DIR/$CFG_PACKAGE_NAME" -need_ok "couldn't delete work package dir" - -mkdir -p "$CFG_WORK_DIR/$CFG_PACKAGE_NAME/$CFG_COMPONENT_NAME" -need_ok "couldn't create work package dir" - -cp -r "$CFG_IMAGE_DIR/"* "$CFG_WORK_DIR/$CFG_PACKAGE_NAME/$CFG_COMPONENT_NAME" -need_ok "couldn't copy source image" - -# Create the manifest -manifest=`(cd "$CFG_WORK_DIR/$CFG_PACKAGE_NAME/$CFG_COMPONENT_NAME" && find . -type f | sed 's/^\.\///') | sort` - -# Remove files in bulk dirs -bulk_dirs=`echo "$CFG_BULK_DIRS" | tr "," " "` -for bulk_dir in $bulk_dirs; do - bulk_dir=`echo "$bulk_dir" | sed s/\\\//\\\\\\\\\\\//g` - manifest=`echo "$manifest" | sed /^$bulk_dir/d` -done - -# Add 'file:' installation directives, skipping empty lines. -manifest=`echo "$manifest" | sed /^$/d | sed s/^/file:/` - -# Add 'dir:' directives -for bulk_dir in $bulk_dirs; do - manifest=`echo "$manifest" && echo "dir:$bulk_dir"` -done - -# The above step may have left a leading empty line if there were only -# bulk dirs. Remove it. -manifest=`echo "$manifest" | sed /^$/d` - -manifest_file="$CFG_WORK_DIR/$CFG_PACKAGE_NAME/$CFG_COMPONENT_NAME/manifest.in" -component_file="$CFG_WORK_DIR/$CFG_PACKAGE_NAME/components" -version_file="$CFG_WORK_DIR/$CFG_PACKAGE_NAME/rust-installer-version" - -# Write the manifest -echo "$manifest" > "$manifest_file" - -# Write the component name -echo "$CFG_COMPONENT_NAME" > "$component_file" - -# Write the installer version (only used by combine-installers.sh) -echo "$rust_installer_version" > "$version_file" - -# Copy the overlay -if [ -n "$CFG_NON_INSTALLED_OVERLAY" ]; then - overlay_files=`(cd "$CFG_NON_INSTALLED_OVERLAY" && find . -type f)` - for f in $overlay_files; do - if [ -e "$CFG_WORK_DIR/$CFG_PACKAGE_NAME/$f" ]; then err "overlay $f exists"; fi - - cp "$CFG_NON_INSTALLED_OVERLAY/$f" "$CFG_WORK_DIR/$CFG_PACKAGE_NAME/$f" - need_ok "failed to copy overlay $f" - done -fi - -# Generate the install script -"$src_dir/gen-install-script.sh" \ - --product-name="$CFG_PRODUCT_NAME" \ - --rel-manifest-dir="$CFG_REL_MANIFEST_DIR" \ - --success-message="$CFG_SUCCESS_MESSAGE" \ - --legacy-manifest-dirs="$CFG_LEGACY_MANIFEST_DIRS" \ - --output-script="$CFG_WORK_DIR/$CFG_PACKAGE_NAME/install.sh" - -need_ok "failed to generate install script" - -mkdir -p "$CFG_OUTPUT_DIR" -need_ok "couldn't create output dir" - -"$src_dir/make-tarballs.sh" \ - --work-dir="$CFG_WORK_DIR" \ - --input="$CFG_PACKAGE_NAME" \ - --output="$CFG_OUTPUT_DIR/$CFG_PACKAGE_NAME" +cargo run --manifest-path="$src_dir/Cargo.toml" -- generate "$@" diff --git a/make-tarballs.sh b/make-tarballs.sh index b206a27..e9f88cc 100755 --- a/make-tarballs.sh +++ b/make-tarballs.sh @@ -11,193 +11,6 @@ set -ue -msg() { - echo "make-tarballs: ${1-}" -} - -step_msg() { - msg - msg "$1" - msg -} - -warn() { - echo "make-tarballs: WARNING: $1" >&2 -} - -err() { - echo "make-tarballs: error: $1" >&2 - exit 1 -} - -need_ok() { - if [ $? -ne 0 ] - then - err "$1" - fi -} - -need_cmd() { - if command -v $1 >/dev/null 2>&1 - then msg "found $1" - else err "need $1" - fi -} - -putvar() { - local t - local tlen - eval t=\$$1 - eval tlen=\${#$1} - if [ $tlen -gt 35 ] - then - printf "make-tarballs: %-20s := %.35s ...\n" $1 "$t" - else - printf "make-tarballs: %-20s := %s %s\n" $1 "$t" - fi -} - -valopt() { - VAL_OPTIONS="$VAL_OPTIONS $1" - - local op=$1 - local default=$2 - shift - shift - local doc="$*" - if [ $HELP -eq 0 ] - then - local uop=$(echo $op | tr '[:lower:]' '[:upper:]' | tr '\-' '\_') - local v="CFG_${uop}" - eval $v="$default" - for arg in $CFG_ARGS - do - if echo "$arg" | grep -q -- "--$op=" - then - local val=$(echo "$arg" | cut -f2 -d=) - eval $v=$val - fi - done - putvar $v - else - if [ -z "$default" ] - then - default="" - fi - op="${op}=[${default}]" - printf " --%-30s %s\n" "$op" "$doc" - fi -} - -opt() { - BOOL_OPTIONS="$BOOL_OPTIONS $1" - - local op=$1 - local default=$2 - shift - shift - local doc="$*" - local flag="" - - if [ $default -eq 0 ] - then - flag="enable" - else - flag="disable" - doc="don't $doc" - fi - - if [ $HELP -eq 0 ] - then - for arg in $CFG_ARGS - do - if [ "$arg" = "--${flag}-${op}" ] - then - op=$(echo $op | tr 'a-z-' 'A-Z_') - flag=$(echo $flag | tr 'a-z' 'A-Z') - local v="CFG_${flag}_${op}" - eval $v=1 - putvar $v - fi - done - else - if [ ! -z "$META" ] - then - op="$op=<$META>" - fi - printf " --%-30s %s\n" "$flag-$op" "$doc" - fi -} - -flag() { - BOOL_OPTIONS="$BOOL_OPTIONS $1" - - local op=$1 - shift - local doc="$*" - - if [ $HELP -eq 0 ] - then - for arg in $CFG_ARGS - do - if [ "$arg" = "--${op}" ] - then - op=$(echo $op | tr 'a-z-' 'A-Z_') - local v="CFG_${op}" - eval $v=1 - putvar $v - fi - done - else - if [ ! -z "$META" ] - then - op="$op=<$META>" - fi - printf " --%-30s %s\n" "$op" "$doc" - fi -} - -validate_opt () { - for arg in $CFG_ARGS - do - local is_arg_valid=0 - for option in $BOOL_OPTIONS - do - if test --disable-$option = $arg - then - is_arg_valid=1 - fi - if test --enable-$option = $arg - then - is_arg_valid=1 - fi - if test --$option = $arg - then - is_arg_valid=1 - fi - done - for option in $VAL_OPTIONS - do - if echo "$arg" | grep -q -- "--$option=" - then - is_arg_valid=1 - fi - done - if [ "$arg" = "--help" ] - then - echo - echo "No more help available for Configure options," - echo "check the Wiki or join our IRC channel" - break - else - if test $is_arg_valid -eq 0 - then - err "Option '$arg' is not recognized" - fi - fi - done -} - # Prints the absolute path of a directory to stdout abs_path() { local path="$1" @@ -207,76 +20,5 @@ abs_path() { (unset CDPATH && cd "$path" > /dev/null && pwd) } -msg "looking for programs" -msg - -need_cmd tar -need_cmd rm -need_cmd mkdir -need_cmd echo -need_cmd tr -need_cmd find -need_cmd rev -need_cmd sort -need_cmd gzip - -# need_cmd xz || need_cmd 7z -if command -v xz >/dev/null 2>&1 -then msg "found xz" -else need_cmd 7z -fi - -CFG_ARGS="$@" - -HELP=0 -if [ "$1" = "--help" ] -then - HELP=1 - shift - echo - echo "Usage: $0 [options]" - echo - echo "Options:" - echo -else - step_msg "processing arguments" -fi - -OPTIONS="" -BOOL_OPTIONS="" -VAL_OPTIONS="" - -valopt input "package" "The input folder to be compressed" -valopt output "./dist" "The prefix of the tarballs" -valopt work-dir "./workdir" "The folder in which the input is to be found" - -if [ $HELP -eq 1 ] -then - echo - exit 0 -fi - -step_msg "validating arguments" -validate_opt - -rm -Rf "$CFG_OUTPUT.tar.gz" -need_ok "couldn't delete old gz tarball" - -rm -Rf "$CFG_OUTPUT.tar.xz" -need_ok "couldn't delete old xz tarball" - -# Make a tarball -cd "$CFG_WORK_DIR" - -tar -cf "$CFG_OUTPUT.tar" "$CFG_INPUT" - -need_ok "failed to tar" - -if command -v xz >/dev/null 2>&1 -then xz -9 --keep "$CFG_OUTPUT.tar" -else 7z a -bd -txz -mx=9 -mmt=off "$CFG_OUTPUT.tar.xz" "$CFG_OUTPUT.tar" -fi -need_ok "failed to xz" - -gzip "$CFG_OUTPUT.tar" -need_ok "failed to gzip" +src_dir="$(abs_path $(dirname "$0"))" +cargo run --manifest-path="$src_dir/Cargo.toml" -- tarball "$@" diff --git a/src/combiner.rs b/src/combiner.rs new file mode 100644 index 0000000..d084a85 --- /dev/null +++ b/src/combiner.rs @@ -0,0 +1,134 @@ +// Copyright 2017 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use std::io::{Read, Write}; +use std::path::Path; +use flate2::read::GzDecoder; +use tar::Archive; + +use errors::*; +use super::Scripter; +use super::Tarballer; +use util::*; + +actor!{ + #[derive(Debug)] + pub struct Combiner { + /// The name of the product, for display + product_name: String = "Product", + + /// The name of the package, tarball + package_name: String = "package", + + /// The directory under lib/ where the manifest lives + rel_manifest_dir: String = "packagelib", + + /// The string to print after successful installation + success_message: String = "Installed.", + + /// Places to look for legacy manifests to uninstall + legacy_manifest_dirs: String = "", + + /// Installers to combine + input_tarballs: String = "", + + /// Directory containing files that should not be installed + non_installed_overlay: String = "", + + /// The directory to do temporary work + work_dir: String = "./workdir", + + /// The location to put the final image and tarball + output_dir: String = "./dist", + } +} + +impl Combiner { + /// Combine the installer tarballs + pub fn run(self) -> Result<()> { + create_dir_all(&self.work_dir)?; + + let package_dir = Path::new(&self.work_dir).join(&self.package_name); + if package_dir.exists() { + remove_dir_all(&package_dir)?; + } + create_dir_all(&package_dir)?; + + // Merge each installer into the work directory of the new installer + let components = create_new_file(package_dir.join("components"))?; + for input_tarball in self.input_tarballs.split(',').map(str::trim).filter(|s| !s.is_empty()) { + // Extract the input tarballs + GzDecoder::new(open_file(&input_tarball)?) + .and_then(|tar| Archive::new(tar).unpack(&self.work_dir)) + .chain_err(|| format!("unable to extract '{}' into '{}'", + &input_tarball, self.work_dir))?; + + let pkg_name = input_tarball.trim_right_matches(".tar.gz"); + let pkg_name = Path::new(pkg_name).file_name().unwrap(); + let pkg_dir = Path::new(&self.work_dir).join(&pkg_name); + + // Verify the version number + let mut version = String::new(); + open_file(pkg_dir.join("rust-installer-version")) + .and_then(|mut file| file.read_to_string(&mut version).map_err(Error::from)) + .chain_err(|| format!("failed to read version in '{}'", input_tarball))?; + if version.trim().parse() != Ok(::RUST_INSTALLER_VERSION) { + bail!("incorrect installer version in {}", input_tarball); + } + + // Move components to the new combined installer + let mut pkg_components = String::new(); + open_file(pkg_dir.join("components")) + .and_then(|mut file| file.read_to_string(&mut pkg_components).map_err(Error::from)) + .chain_err(|| format!("failed to read components in '{}'", input_tarball))?; + for component in pkg_components.split_whitespace() { + // All we need to do is move the component directory + let component_dir = package_dir.join(&component); + rename(&pkg_dir.join(&component), component_dir)?; + + // Merge the component name + writeln!(&components, "{}", component) + .chain_err(|| "failed to write new components")?; + } + } + drop(components); + + // Write the installer version + let version = package_dir.join("rust-installer-version"); + writeln!(create_new_file(version)?, "{}", ::RUST_INSTALLER_VERSION) + .chain_err(|| "failed to write new installer version")?; + + // Copy the overlay + if !self.non_installed_overlay.is_empty() { + copy_recursive(self.non_installed_overlay.as_ref(), &package_dir)?; + } + + // Generate the install script + let output_script = package_dir.join("install.sh"); + let mut scripter = Scripter::default(); + scripter.product_name(self.product_name) + .rel_manifest_dir(self.rel_manifest_dir) + .success_message(self.success_message) + .legacy_manifest_dirs(self.legacy_manifest_dirs) + .output_script(path_to_str(&output_script)?); + scripter.run()?; + + // Make the tarballs + create_dir_all(&self.output_dir)?; + let output = Path::new(&self.output_dir).join(&self.package_name); + let mut tarballer = Tarballer::default(); + tarballer.work_dir(self.work_dir) + .input(self.package_name) + .output(path_to_str(&output)?); + tarballer.run()?; + + Ok(()) + } +} diff --git a/src/generator.rs b/src/generator.rs new file mode 100644 index 0000000..f5be602 --- /dev/null +++ b/src/generator.rs @@ -0,0 +1,129 @@ +// Copyright 2017 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use std::io::Write; +use std::path::Path; + +use errors::*; +use super::Scripter; +use super::Tarballer; +use util::*; + +actor!{ + #[derive(Debug)] + pub struct Generator { + /// The name of the product, for display + product_name: String = "Product", + + /// The name of the component, distinct from other installed components + component_name: String = "component", + + /// The name of the package, tarball + package_name: String = "package", + + /// The directory under lib/ where the manifest lives + rel_manifest_dir: String = "packagelib", + + /// The string to print after successful installation + success_message: String = "Installed.", + + /// Places to look for legacy manifests to uninstall + legacy_manifest_dirs: String = "", + + /// Directory containing files that should not be installed + non_installed_overlay: String = "", + + /// Path prefixes of directories that should be installed/uninstalled in bulk + bulk_dirs: String = "", + + /// The directory containing the installation medium + image_dir: String = "./install_image", + + /// The directory to do temporary work + work_dir: String = "./workdir", + + /// The location to put the final image and tarball + output_dir: String = "./dist", + } +} + +impl Generator { + /// Generate the actual installer tarball + pub fn run(self) -> Result<()> { + create_dir_all(&self.work_dir)?; + + let package_dir = Path::new(&self.work_dir).join(&self.package_name); + if package_dir.exists() { + remove_dir_all(&package_dir)?; + } + + // Copy the image and write the manifest + let component_dir = package_dir.join(&self.component_name); + create_dir_all(&component_dir)?; + copy_and_manifest(self.image_dir.as_ref(), &component_dir, &self.bulk_dirs)?; + + // Write the component name + let components = package_dir.join("components"); + writeln!(create_new_file(components)?, "{}", self.component_name) + .chain_err(|| "failed to write the component file")?; + + // Write the installer version (only used by combine-installers.sh) + let version = package_dir.join("rust-installer-version"); + writeln!(create_new_file(version)?, "{}", ::RUST_INSTALLER_VERSION) + .chain_err(|| "failed to write new installer version")?; + + // Copy the overlay + if !self.non_installed_overlay.is_empty() { + copy_recursive(self.non_installed_overlay.as_ref(), &package_dir)?; + } + + // Generate the install script + let output_script = package_dir.join("install.sh"); + let mut scripter = Scripter::default(); + scripter.product_name(self.product_name) + .rel_manifest_dir(self.rel_manifest_dir) + .success_message(self.success_message) + .legacy_manifest_dirs(self.legacy_manifest_dirs) + .output_script(path_to_str(&output_script)?); + scripter.run()?; + + // Make the tarballs + create_dir_all(&self.output_dir)?; + let output = Path::new(&self.output_dir).join(&self.package_name); + let mut tarballer = Tarballer::default(); + tarballer.work_dir(self.work_dir) + .input(self.package_name) + .output(path_to_str(&output)?); + tarballer.run()?; + + Ok(()) + } +} + +/// Copies the `src` directory recursively to `dst`, writing `manifest.in` too. +fn copy_and_manifest(src: &Path, dst: &Path, bulk_dirs: &str) -> Result<()> { + let manifest = create_new_file(dst.join("manifest.in"))?; + let bulk_dirs: Vec<_> = bulk_dirs.split(',') + .filter(|s| !s.is_empty()) + .map(Path::new).collect(); + + copy_with_callback(src, dst, |path, file_type| { + if file_type.is_dir() { + if bulk_dirs.contains(&path) { + writeln!(&manifest, "dir:{}", path.display())?; + } + } else { + if !bulk_dirs.iter().any(|d| path.starts_with(d)) { + writeln!(&manifest, "file:{}", path.display())?; + } + } + Ok(()) + }) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b9375df --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,55 @@ +// Copyright 2017 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#[macro_use] +extern crate error_chain; +extern crate flate2; +extern crate tar; +extern crate walkdir; +extern crate xz2; + +#[cfg(windows)] +extern crate winapi; +#[cfg(windows)] +extern crate kernel32; +#[cfg(windows)] +#[macro_use] +extern crate lazy_static; + +mod errors { + error_chain!{ + foreign_links { + Io(::std::io::Error); + StripPrefix(::std::path::StripPrefixError); + WalkDir(::walkdir::Error); + } + } +} + +#[macro_use] +mod util; + +// deal with OS complications (cribbed from rustup.rs) +mod remove_dir_all; + +mod combiner; +mod generator; +mod scripter; +mod tarballer; + +pub use errors::{Result, Error, ErrorKind}; +pub use combiner::Combiner; +pub use generator::Generator; +pub use scripter::Scripter; +pub use tarballer::Tarballer; + +/// The installer version, output only to be used by combine-installers.sh. +/// (should match `SOURCE_DIRECTORY/rust_installer_version`) +pub const RUST_INSTALLER_VERSION: u32 = 3; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..cabffb8 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,98 @@ +#[macro_use] +extern crate clap; +#[macro_use] +extern crate error_chain; +extern crate installer; + +use errors::*; +use clap::{App, ArgMatches}; + +mod errors { + error_chain!{ + links { + Installer(::installer::Error, ::installer::ErrorKind); + } + } +} + +quick_main!(run); + +fn run() -> Result<()> { + let yaml = load_yaml!("main.yml"); + let matches = App::from_yaml(yaml).get_matches(); + + match matches.subcommand() { + ("combine", Some(matches)) => combine(matches), + ("generate", Some(matches)) => generate(matches), + ("script", Some(matches)) => script(matches), + ("tarball", Some(matches)) => tarball(matches), + _ => unreachable!(), + } +} + +/// Parse clap arguements into the type constructor. +macro_rules! parse( + ($matches:expr => $type:ty { $( $option:tt => $setter:ident, )* }) => { + { + let mut command: $type = Default::default(); + $( $matches.value_of($option).map(|s| command.$setter(s)); )* + command + } + } +); + +fn combine(matches: &ArgMatches) -> Result<()> { + let combiner = parse!(matches => installer::Combiner { + "product-name" => product_name, + "package-name" => package_name, + "rel-manifest-dir" => rel_manifest_dir, + "success-message" => success_message, + "legacy-manifest-dirs" => legacy_manifest_dirs, + "input-tarballs" => input_tarballs, + "non-installed-overlay" => non_installed_overlay, + "work-dir" => work_dir, + "output-dir" => output_dir, + }); + + combiner.run().chain_err(|| "failed to combine installers") +} + +fn generate(matches: &ArgMatches) -> Result<()> { + let generator = parse!(matches => installer::Generator { + "product-name" => product_name, + "component-name" => component_name, + "package-name" => package_name, + "rel-manifest-dir" => rel_manifest_dir, + "success-message" => success_message, + "legacy-manifest-dirs" => legacy_manifest_dirs, + "non-installed-overlay" => non_installed_overlay, + "bulk-dirs" => bulk_dirs, + "image-dir" => image_dir, + "work-dir" => work_dir, + "output-dir" => output_dir, + }); + + generator.run().chain_err(|| "failed to generate installer") +} + +fn script(matches: &ArgMatches) -> Result<()> { + let scripter = parse!(matches => installer::Scripter { + "product-name" => product_name, + "rel-manifest-dir" => rel_manifest_dir, + "success-message" => success_message, + "legacy-manifest-dirs" => legacy_manifest_dirs, + "output-script" => output_script, + }); + + scripter.run().chain_err(|| "failed to generate installation script") +} + +fn tarball(matches: &ArgMatches) -> Result<()> { + let tarballer = parse!(matches => installer::Tarballer { + "input" => input, + "output" => output, + "work-dir" => work_dir, + }); + + tarballer.run().chain_err(|| "failed to generate tarballs") +} diff --git a/src/main.yml b/src/main.yml new file mode 100644 index 0000000..2f9978b --- /dev/null +++ b/src/main.yml @@ -0,0 +1,157 @@ +name: installer +settings: + - ArgRequiredElseHelp +subcommands: + - generate: + about: Generate a complete installer tarball + args: + - product-name: + help: The name of the product, for display + long: product-name + takes_value: true + value_name: NAME + - component-name: + help: The name of the component, distinct from other installed components + long: component-name + takes_value: true + value_name: NAME + - package-name: + help: The name of the package, tarball + long: package-name + takes_value: true + value_name: NAME + - rel-manifest-dir: + help: The directory under lib/ where the manifest lives + long: rel-manifest-dir + takes_value: true + value_name: DIR + - success-message: + help: The string to print after successful installation + long: success-message + takes_value: true + value_name: MESSAGE + - legacy-manifest-dirs: + help: Places to look for legacy manifests to uninstall + long: legacy-manifest-dirs + takes_value: true + value_name: DIRS + - non-installed-overlay: + help: Directory containing files that should not be installed + long: non-installed-overlay + takes_value: true + value_name: DIR + - bulk-dirs: + help: Path prefixes of directories that should be installed/uninstalled in bulk + long: bulk-dirs + takes_value: true + value_name: DIRS + - image-dir: + help: The directory containing the installation medium + long: image-dir + takes_value: true + value_name: DIR + - work-dir: + help: The directory to do temporary work + long: work-dir + takes_value: true + value_name: DIR + - output-dir: + help: The location to put the final image and tarball + long: output-dir + takes_value: true + value_name: DIR + - combine: + about: Combine installer tarballs + args: + - product-name: + help: The name of the product, for display + long: product-name + takes_value: true + value_name: NAME + - package-name: + help: The name of the package, tarball + long: package-name + takes_value: true + value_name: NAME + - rel-manifest-dir: + help: The directory under lib/ where the manifest lives + long: rel-manifest-dir + takes_value: true + value_name: DIR + - success-message: + help: The string to print after successful installation + long: success-message + takes_value: true + value_name: MESSAGE + - legacy-manifest-dirs: + help: Places to look for legacy manifests to uninstall + long: legacy-manifest-dirs + takes_value: true + value_name: DIRS + - input-tarballs: + help: Installers to combine + long: input-tarballs + takes_value: true + value_name: FILE,FILE + - non-installed-overlay: + help: Directory containing files that should not be installed + long: non-installed-overlay + takes_value: true + value_name: DIR + - work-dir: + help: The directory to do temporary work + long: work-dir + takes_value: true + value_name: DIR + - output-dir: + help: The location to put the final image and tarball + long: output-dir + takes_value: true + value_name: DIR + - script: + about: Generate an installation script + args: + - product-name: + help: The name of the product, for display + long: product-name + takes_value: true + value_name: NAME + - rel-manifest-dir: + help: The directory under lib/ where the manifest lives + long: rel-manifest-dir + takes_value: true + value_name: DIR + - success-message: + help: The string to print after successful installation + long: success-message + takes_value: true + value_name: MESSAGE + - legacy-manifest-dirs: + help: Places to look for legacy manifests to uninstall + long: legacy-manifest-dirs + takes_value: true + value_name: DIRS + - output-script: + help: The name of the output script + long: output-script + takes_value: true + value_name: FILE + - tarball: + about: Generate package tarballs + args: + - input: + help: The input folder to be compressed + long: input + takes_value: true + value_name: NAME + - output: + help: The prefix of the tarballs + long: output + takes_value: true + value_name: PATH + - work-dir: + help: The folder in which the input is to be found + long: work-dir + takes_value: true + value_name: DIR + diff --git a/src/remove_dir_all.rs b/src/remove_dir_all.rs new file mode 100644 index 0000000..778b3a1 --- /dev/null +++ b/src/remove_dir_all.rs @@ -0,0 +1,835 @@ +// Copyright 2014 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +#![allow(non_snake_case)] + +use std::path::Path; +use std::io; + +#[cfg(not(windows))] +pub fn remove_dir_all(path: &Path) -> io::Result<()> { + ::std::fs::remove_dir_all(path) +} + +#[cfg(windows)] +pub fn remove_dir_all(path: &Path) -> io::Result<()> { + win::remove_dir_all(path) +} + +#[cfg(windows)] +mod win { + use winapi::{ + FileBasicInfo, + FILE_BASIC_INFO, + FALSE, + FileRenameInfo, + FILE_RENAME_INFO, + c_ushort, + c_uint, + FILETIME, + FILE_ATTRIBUTE_READONLY, + FILE_ATTRIBUTE_REPARSE_POINT, + FILE_ATTRIBUTE_DIRECTORY, + WIN32_FIND_DATAW, + ERROR_NO_MORE_FILES, + OPEN_EXISTING, + OPEN_ALWAYS, + TRUNCATE_EXISTING, + CREATE_ALWAYS, + CREATE_NEW, + GENERIC_READ, + GENERIC_WRITE, + FILE_GENERIC_WRITE, + FILE_WRITE_DATA, + FILE_SHARE_READ, + FILE_SHARE_WRITE, + FILE_SHARE_DELETE, + FILE_FLAG_DELETE_ON_CLOSE, + DELETE, + FILE_WRITE_ATTRIBUTES, + FILE_INFO_BY_HANDLE_CLASS, + HANDLE, + ERROR_INSUFFICIENT_BUFFER, + FILE_READ_ATTRIBUTES, + FILE_FLAG_BACKUP_SEMANTICS, + FILE_FLAG_OPEN_REPARSE_POINT, + ERROR_CALL_NOT_IMPLEMENTED, + DWORD, + BOOL, + LPVOID, + INVALID_HANDLE_VALUE, + LPCWSTR, + SECURITY_SQOS_PRESENT, + FSCTL_GET_REPARSE_POINT, + BY_HANDLE_FILE_INFORMATION, + IO_REPARSE_TAG_SYMLINK, + IO_REPARSE_TAG_MOUNT_POINT, + }; + + use kernel32::{ + CreateFileW, + GetFileInformationByHandle, + CloseHandle, + GetLastError, + SetLastError, + DeviceIoControl, + GetModuleHandleW, + GetProcAddress, + FindNextFileW, + FindFirstFileW, + }; + + use std::ptr; + use std::sync::Arc; + use std::path::{PathBuf, Path}; + use std::mem; + use std::io; + use std::ffi::{OsStr, OsString}; + use std::os::windows::ffi::{OsStrExt, OsStringExt}; + + pub fn remove_dir_all(path: &Path) -> io::Result<()> { + // On Windows it is not enough to just recursively remove the contents of a + // directory and then the directory itself. Deleting does not happen + // instantaneously, but is scheduled. + // To work around this, we move the file or directory to some `base_dir` + // right before deletion to avoid races. + // + // As `base_dir` we choose the parent dir of the directory we want to + // remove. We very probably have permission to create files here, as we + // already need write permission in this dir to delete the directory. And it + // should be on the same volume. + // + // To handle files with names like `CON` and `morse .. .`, and when a + // directory structure is so deep it needs long path names the path is first + // converted to a `//?/`-path with `get_path()`. + // + // To make sure we don't leave a moved file laying around if the process + // crashes before we can delete the file, we do all operations on an file + // handle. By opening a file with `FILE_FLAG_DELETE_ON_CLOSE` Windows will + // always delete the file when the handle closes. + // + // All files are renamed to be in the `base_dir`, and have their name + // changed to "rm-". After every rename the counter is increased. + // Rename should not overwrite possibly existing files in the base dir. So + // if it fails with `AlreadyExists`, we just increase the counter and try + // again. + // + // For read-only files and directories we first have to remove the read-only + // attribute before we can move or delete them. This also removes the + // attribute from possible hardlinks to the file, so just before closing we + // restore the read-only attribute. + // + // If 'path' points to a directory symlink or junction we should not + // recursively remove the target of the link, but only the link itself. + // + // Moving and deleting is guaranteed to succeed if we are able to open the + // file with `DELETE` permission. If others have the file open we only have + // `DELETE` permission if they have specified `FILE_SHARE_DELETE`. We can + // also delete the file now, but it will not disappear until all others have + // closed the file. But no-one can open the file after we have flagged it + // for deletion. + + // Open the path once to get the canonical path, file type and attributes. + let (path, metadata) = { + let mut opts = OpenOptions::new(); + opts.access_mode(FILE_READ_ATTRIBUTES); + opts.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | + FILE_FLAG_OPEN_REPARSE_POINT); + let file = try!(File::open(path, &opts)); + (try!(get_path(&file)), try!(file.file_attr())) + }; + + let mut ctx = RmdirContext { + base_dir: match path.parent() { + Some(dir) => dir, + None => return Err(io::Error::new(io::ErrorKind::PermissionDenied, + "can't delete root directory")) + }, + readonly: metadata.perm().readonly(), + counter: 0, + }; + + let filetype = metadata.file_type(); + if filetype.is_dir() { + remove_dir_all_recursive(path.as_ref(), &mut ctx) + } else if filetype.is_symlink_dir() { + remove_item(path.as_ref(), &mut ctx) + } else { + Err(io::Error::new(io::ErrorKind::PermissionDenied, "Not a directory")) + } + } + + + fn readdir(p: &Path) -> io::Result { + let root = p.to_path_buf(); + let star = p.join("*"); + let path = try!(to_u16s(&star)); + + unsafe { + let mut wfd = mem::zeroed(); + let find_handle = FindFirstFileW(path.as_ptr(), &mut wfd); + if find_handle != INVALID_HANDLE_VALUE { + Ok(ReadDir { + handle: FindNextFileHandle(find_handle), + root: Arc::new(root), + first: Some(wfd), + }) + } else { + Err(io::Error::last_os_error()) + } + } + } + + struct RmdirContext<'a> { + base_dir: &'a Path, + readonly: bool, + counter: u64, + } + + fn remove_dir_all_recursive(path: &Path, ctx: &mut RmdirContext) + -> io::Result<()> { + let dir_readonly = ctx.readonly; + for child in try!(readdir(path)) { + let child = try!(child); + let child_type = try!(child.file_type()); + ctx.readonly = try!(child.metadata()).perm().readonly(); + if child_type.is_dir() { + try!(remove_dir_all_recursive(&child.path(), ctx)); + } else { + try!(remove_item(&child.path().as_ref(), ctx)); + } + } + ctx.readonly = dir_readonly; + remove_item(path, ctx) + } + + fn remove_item(path: &Path, ctx: &mut RmdirContext) -> io::Result<()> { + if !ctx.readonly { + let mut opts = OpenOptions::new(); + opts.access_mode(DELETE); + opts.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | // delete directory + FILE_FLAG_OPEN_REPARSE_POINT | // delete symlink + FILE_FLAG_DELETE_ON_CLOSE); + let file = try!(File::open(path, &opts)); + move_item(&file, ctx) + } else { + // remove read-only permision + try!(set_perm(&path, FilePermissions::new())); + // move and delete file, similar to !readonly. + // only the access mode is different. + let mut opts = OpenOptions::new(); + opts.access_mode(DELETE | FILE_WRITE_ATTRIBUTES); + opts.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | + FILE_FLAG_OPEN_REPARSE_POINT | + FILE_FLAG_DELETE_ON_CLOSE); + let file = try!(File::open(path, &opts)); + try!(move_item(&file, ctx)); + // restore read-only flag just in case there are other hard links + let mut perm = FilePermissions::new(); + perm.set_readonly(true); + let _ = file.set_perm(perm); // ignore if this fails + Ok(()) + } + } + + macro_rules! compat_fn { + ($module:ident: $( + fn $symbol:ident($($argname:ident: $argtype:ty),*) + -> $rettype:ty { + $($body:expr);* + } + )*) => ($( + #[allow(unused_variables)] + unsafe fn $symbol($($argname: $argtype),*) -> $rettype { + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::mem; + use std::ffi::CString; + type F = unsafe extern "system" fn($($argtype),*) -> $rettype; + + lazy_static! { static ref PTR: AtomicUsize = AtomicUsize::new(0);} + + fn lookup(module: &str, symbol: &str) -> Option { + let mut module: Vec = module.encode_utf16().collect(); + module.push(0); + let symbol = CString::new(symbol).unwrap(); + unsafe { + let handle = GetModuleHandleW(module.as_ptr()); + match GetProcAddress(handle, symbol.as_ptr()) as usize { + 0 => None, + n => Some(n), + } + } + } + + fn store_func(ptr: &AtomicUsize, module: &str, symbol: &str, + fallback: usize) -> usize { + let value = lookup(module, symbol).unwrap_or(fallback); + ptr.store(value, Ordering::SeqCst); + value + } + + fn load() -> usize { + store_func(&PTR, stringify!($module), stringify!($symbol), fallback as usize) + } + unsafe extern "system" fn fallback($($argname: $argtype),*) + -> $rettype { + $($body);* + } + + let addr = match PTR.load(Ordering::SeqCst) { + 0 => load(), + n => n, + }; + mem::transmute::(addr)($($argname),*) + } + )*) + } + + compat_fn! { + kernel32: + fn GetFinalPathNameByHandleW(_hFile: HANDLE, + _lpszFilePath: LPCWSTR, + _cchFilePath: DWORD, + _dwFlags: DWORD) -> DWORD { + SetLastError(ERROR_CALL_NOT_IMPLEMENTED as DWORD); 0 + } + fn SetFileInformationByHandle(_hFile: HANDLE, + _FileInformationClass: FILE_INFO_BY_HANDLE_CLASS, + _lpFileInformation: LPVOID, + _dwBufferSize: DWORD) -> BOOL { + SetLastError(ERROR_CALL_NOT_IMPLEMENTED as DWORD); 0 + } + } + + fn cvt(i: i32) -> io::Result { + if i == 0 { + Err(io::Error::last_os_error()) + } else { + Ok(i) + } + } + + fn to_u16s>(s: S) -> io::Result> { + fn inner(s: &OsStr) -> io::Result> { + let mut maybe_result: Vec = s.encode_wide().collect(); + if maybe_result.iter().any(|&u| u == 0) { + return Err(io::Error::new(io::ErrorKind::InvalidInput, + "strings passed to WinAPI cannot contain NULs")); + } + maybe_result.push(0); + Ok(maybe_result) + } + inner(s.as_ref()) + } + + fn truncate_utf16_at_nul<'a>(v: &'a [u16]) -> &'a [u16] { + match v.iter().position(|c| *c == 0) { + // don't include the 0 + Some(i) => &v[..i], + None => v + } + } + + fn fill_utf16_buf(mut f1: F1, f2: F2) -> io::Result + where F1: FnMut(*mut u16, DWORD) -> DWORD, + F2: FnOnce(&[u16]) -> T + { + // Start off with a stack buf but then spill over to the heap if we end up + // needing more space. + let mut stack_buf = [0u16; 512]; + let mut heap_buf = Vec::new(); + unsafe { + let mut n = stack_buf.len(); + loop { + let buf = if n <= stack_buf.len() { + &mut stack_buf[..] + } else { + let extra = n - heap_buf.len(); + heap_buf.reserve(extra); + heap_buf.set_len(n); + &mut heap_buf[..] + }; + + // This function is typically called on windows API functions which + // will return the correct length of the string, but these functions + // also return the `0` on error. In some cases, however, the + // returned "correct length" may actually be 0! + // + // To handle this case we call `SetLastError` to reset it to 0 and + // then check it again if we get the "0 error value". If the "last + // error" is still 0 then we interpret it as a 0 length buffer and + // not an actual error. + SetLastError(0); + let k = match f1(buf.as_mut_ptr(), n as DWORD) { + 0 if GetLastError() == 0 => 0, + 0 => return Err(io::Error::last_os_error()), + n => n, + } as usize; + if k == n && GetLastError() == ERROR_INSUFFICIENT_BUFFER { + n *= 2; + } else if k >= n { + n = k; + } else { + return Ok(f2(&buf[..k])) + } + } + } + } + + #[derive(Clone, PartialEq, Eq, Debug, Default)] + struct FilePermissions { readonly: bool } + + impl FilePermissions { + fn new() -> FilePermissions { Default::default() } + fn readonly(&self) -> bool { self.readonly } + fn set_readonly(&mut self, readonly: bool) { self.readonly = readonly } + } + + #[derive(Clone)] + struct OpenOptions { + // generic + read: bool, + write: bool, + append: bool, + truncate: bool, + create: bool, + create_new: bool, + // system-specific + custom_flags: u32, + access_mode: Option, + attributes: DWORD, + share_mode: DWORD, + security_qos_flags: DWORD, + security_attributes: usize, // FIXME: should be a reference + } + + impl OpenOptions { + fn new() -> OpenOptions { + OpenOptions { + // generic + read: false, + write: false, + append: false, + truncate: false, + create: false, + create_new: false, + // system-specific + custom_flags: 0, + access_mode: None, + share_mode: FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + attributes: 0, + security_qos_flags: 0, + security_attributes: 0, + } + } + fn custom_flags(&mut self, flags: u32) { self.custom_flags = flags; } + fn access_mode(&mut self, access_mode: u32) { self.access_mode = Some(access_mode); } + + fn get_access_mode(&self) -> io::Result { + const ERROR_INVALID_PARAMETER: i32 = 87; + + match (self.read, self.write, self.append, self.access_mode) { + (_, _, _, Some(mode)) => Ok(mode), + (true, false, false, None) => Ok(GENERIC_READ), + (false, true, false, None) => Ok(GENERIC_WRITE), + (true, true, false, None) => Ok(GENERIC_READ | GENERIC_WRITE), + (false, _, true, None) => Ok(FILE_GENERIC_WRITE & !FILE_WRITE_DATA), + (true, _, true, None) => Ok(GENERIC_READ | + (FILE_GENERIC_WRITE & !FILE_WRITE_DATA)), + (false, false, false, None) => Err(io::Error::from_raw_os_error(ERROR_INVALID_PARAMETER)), + } + } + + fn get_creation_mode(&self) -> io::Result { + const ERROR_INVALID_PARAMETER: i32 = 87; + + match (self.write, self.append) { + (true, false) => {} + (false, false) => + if self.truncate || self.create || self.create_new { + return Err(io::Error::from_raw_os_error(ERROR_INVALID_PARAMETER)); + }, + (_, true) => + if self.truncate && !self.create_new { + return Err(io::Error::from_raw_os_error(ERROR_INVALID_PARAMETER)); + }, + } + + Ok(match (self.create, self.truncate, self.create_new) { + (false, false, false) => OPEN_EXISTING, + (true, false, false) => OPEN_ALWAYS, + (false, true, false) => TRUNCATE_EXISTING, + (true, true, false) => CREATE_ALWAYS, + (_, _, true) => CREATE_NEW, + }) + } + + fn get_flags_and_attributes(&self) -> DWORD { + self.custom_flags | + self.attributes | + self.security_qos_flags | + if self.security_qos_flags != 0 { SECURITY_SQOS_PRESENT } else { 0 } | + if self.create_new { FILE_FLAG_OPEN_REPARSE_POINT } else { 0 } + } + } + + struct File { handle: Handle } + + impl File { + fn open(path: &Path, opts: &OpenOptions) -> io::Result { + let path = try!(to_u16s(path)); + let handle = unsafe { + CreateFileW(path.as_ptr(), + try!(opts.get_access_mode()), + opts.share_mode, + opts.security_attributes as *mut _, + try!(opts.get_creation_mode()), + opts.get_flags_and_attributes(), + ptr::null_mut()) + }; + if handle == INVALID_HANDLE_VALUE { + Err(io::Error::last_os_error()) + } else { + Ok(File { handle: Handle::new(handle) }) + } + } + + fn file_attr(&self) -> io::Result { + unsafe { + let mut info: BY_HANDLE_FILE_INFORMATION = mem::zeroed(); + try!(cvt(GetFileInformationByHandle(self.handle.raw(), + &mut info))); + let mut attr = FileAttr { + attributes: info.dwFileAttributes, + creation_time: info.ftCreationTime, + last_access_time: info.ftLastAccessTime, + last_write_time: info.ftLastWriteTime, + file_size: ((info.nFileSizeHigh as u64) << 32) | (info.nFileSizeLow as u64), + reparse_tag: 0, + }; + if attr.is_reparse_point() { + let mut b = [0; MAXIMUM_REPARSE_DATA_BUFFER_SIZE]; + if let Ok((_, buf)) = self.reparse_point(&mut b) { + attr.reparse_tag = buf.ReparseTag; + } + } + Ok(attr) + } + } + + fn set_attributes(&self, attr: DWORD) -> io::Result<()> { + let mut info = FILE_BASIC_INFO { + CreationTime: 0, // do not change + LastAccessTime: 0, // do not change + LastWriteTime: 0, // do not change + ChangeTime: 0, // do not change + FileAttributes: attr, + }; + let size = mem::size_of_val(&info); + try!(cvt(unsafe { + SetFileInformationByHandle(self.handle.raw(), + FileBasicInfo, + &mut info as *mut _ as *mut _, + size as DWORD) + })); + Ok(()) + } + + fn rename(&self, new: &Path, replace: bool) -> io::Result<()> { + // &self must be opened with DELETE permission + use std::iter; + #[cfg(target_arch = "x86")] + const STRUCT_SIZE: usize = 12; + #[cfg(target_arch = "x86_64")] + const STRUCT_SIZE: usize = 20; + + // FIXME: check for internal NULs in 'new' + let mut data: Vec = iter::repeat(0u16).take(STRUCT_SIZE/2) + .chain(new.as_os_str().encode_wide()) + .collect(); + data.push(0); + let size = data.len() * 2; + + unsafe { + // Thanks to alignment guarantees on Windows this works + // (8 for 32-bit and 16 for 64-bit) + let mut info = data.as_mut_ptr() as *mut FILE_RENAME_INFO; + // The type of ReplaceIfExists is BOOL, but it actually expects a + // BOOLEAN. This means true is -1, not c::TRUE. + (*info).ReplaceIfExists = if replace { -1 } else { FALSE }; + (*info).RootDirectory = ptr::null_mut(); + (*info).FileNameLength = (size - STRUCT_SIZE) as DWORD; + try!(cvt(SetFileInformationByHandle(self.handle().raw(), + FileRenameInfo, + data.as_mut_ptr() as *mut _ as *mut _, + size as DWORD))); + Ok(()) + } + } + fn set_perm(&self, perm: FilePermissions) -> io::Result<()> { + let attr = try!(self.file_attr()).attributes; + if perm.readonly == (attr & FILE_ATTRIBUTE_READONLY != 0) { + Ok(()) + } else if perm.readonly { + self.set_attributes(attr | FILE_ATTRIBUTE_READONLY) + } else { + self.set_attributes(attr & !FILE_ATTRIBUTE_READONLY) + } + } + + fn handle(&self) -> &Handle { &self.handle } + + fn reparse_point<'a>(&self, + space: &'a mut [u8; MAXIMUM_REPARSE_DATA_BUFFER_SIZE]) + -> io::Result<(DWORD, &'a REPARSE_DATA_BUFFER)> { + unsafe { + let mut bytes = 0; + try!(cvt({ + DeviceIoControl(self.handle.raw(), + FSCTL_GET_REPARSE_POINT, + ptr::null_mut(), + 0, + space.as_mut_ptr() as *mut _, + space.len() as DWORD, + &mut bytes, + ptr::null_mut()) + })); + Ok((bytes, &*(space.as_ptr() as *const REPARSE_DATA_BUFFER))) + } + } + } + + + #[derive(Copy, Clone, PartialEq, Eq, Hash)] + enum FileType { + Dir, File, SymlinkFile, SymlinkDir, ReparsePoint, MountPoint, + } + + impl FileType { + fn new(attrs: DWORD, reparse_tag: DWORD) -> FileType { + match (attrs & FILE_ATTRIBUTE_DIRECTORY != 0, + attrs & FILE_ATTRIBUTE_REPARSE_POINT != 0, + reparse_tag) { + (false, false, _) => FileType::File, + (true, false, _) => FileType::Dir, + (false, true, IO_REPARSE_TAG_SYMLINK) => FileType::SymlinkFile, + (true, true, IO_REPARSE_TAG_SYMLINK) => FileType::SymlinkDir, + (true, true, IO_REPARSE_TAG_MOUNT_POINT) => FileType::MountPoint, + (_, true, _) => FileType::ReparsePoint, + // Note: if a _file_ has a reparse tag of the type IO_REPARSE_TAG_MOUNT_POINT it is + // invalid, as junctions always have to be dirs. We set the filetype to ReparsePoint + // to indicate it is something symlink-like, but not something you can follow. + } + } + + fn is_dir(&self) -> bool { *self == FileType::Dir } + fn is_symlink_dir(&self) -> bool { + *self == FileType::SymlinkDir || *self == FileType::MountPoint + } + } + + impl DirEntry { + fn new(root: &Arc, wfd: &WIN32_FIND_DATAW) -> Option { + let first_bytes = &wfd.cFileName[0..3]; + if first_bytes.starts_with(&[46, 0]) || first_bytes.starts_with(&[46, 46, 0]) { + None + } else { + Some(DirEntry { + root: root.clone(), + data: *wfd, + }) + } + } + + fn path(&self) -> PathBuf { + self.root.join(&self.file_name()) + } + + fn file_name(&self) -> OsString { + let filename = truncate_utf16_at_nul(&self.data.cFileName); + OsString::from_wide(filename) + } + + fn file_type(&self) -> io::Result { + Ok(FileType::new(self.data.dwFileAttributes, + /* reparse_tag = */ self.data.dwReserved0)) + } + + fn metadata(&self) -> io::Result { + Ok(FileAttr { + attributes: self.data.dwFileAttributes, + creation_time: self.data.ftCreationTime, + last_access_time: self.data.ftLastAccessTime, + last_write_time: self.data.ftLastWriteTime, + file_size: ((self.data.nFileSizeHigh as u64) << 32) | (self.data.nFileSizeLow as u64), + reparse_tag: if self.data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 { + // reserved unless this is a reparse point + self.data.dwReserved0 + } else { + 0 + }, + }) + } + } + + + + struct DirEntry { + root: Arc, + data: WIN32_FIND_DATAW, + } + + struct ReadDir { + handle: FindNextFileHandle, + root: Arc, + first: Option, + } + + impl Iterator for ReadDir { + type Item = io::Result; + fn next(&mut self) -> Option> { + if let Some(first) = self.first.take() { + if let Some(e) = DirEntry::new(&self.root, &first) { + return Some(Ok(e)); + } + } + unsafe { + let mut wfd = mem::zeroed(); + loop { + if FindNextFileW(self.handle.0, &mut wfd) == 0 { + if GetLastError() == ERROR_NO_MORE_FILES { + return None + } else { + return Some(Err(io::Error::last_os_error())) + } + } + if let Some(e) = DirEntry::new(&self.root, &wfd) { + return Some(Ok(e)) + } + } + } + } + } + + + #[derive(Clone)] + struct FileAttr { + attributes: DWORD, + creation_time: FILETIME, + last_access_time: FILETIME, + last_write_time: FILETIME, + file_size: u64, + reparse_tag: DWORD, + } + + impl FileAttr { + fn perm(&self) -> FilePermissions { + FilePermissions { + readonly: self.attributes & FILE_ATTRIBUTE_READONLY != 0 + } + } + + fn file_type(&self) -> FileType { + FileType::new(self.attributes, self.reparse_tag) + } + + fn is_reparse_point(&self) -> bool { + self.attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 + } + } + + #[repr(C)] + struct REPARSE_DATA_BUFFER { + ReparseTag: c_uint, + ReparseDataLength: c_ushort, + Reserved: c_ushort, + rest: (), + } + + const MAXIMUM_REPARSE_DATA_BUFFER_SIZE: usize = 16 * 1024; + + + /// An owned container for `HANDLE` object, closing them on Drop. + /// + /// All methods are inherited through a `Deref` impl to `RawHandle` + struct Handle(RawHandle); + + use std::ops::Deref; + + /// A wrapper type for `HANDLE` objects to give them proper Send/Sync inference + /// as well as Rust-y methods. + /// + /// This does **not** drop the handle when it goes out of scope, use `Handle` + /// instead for that. + #[derive(Copy, Clone)] + struct RawHandle(HANDLE); + + unsafe impl Send for RawHandle {} + unsafe impl Sync for RawHandle {} + + impl Handle { + fn new(handle: HANDLE) -> Handle { + Handle(RawHandle::new(handle)) + } + } + + impl Deref for Handle { + type Target = RawHandle; + fn deref(&self) -> &RawHandle { &self.0 } + } + + impl Drop for Handle { + fn drop(&mut self) { + unsafe { let _ = CloseHandle(self.raw()); } + } + } + + impl RawHandle { + fn new(handle: HANDLE) -> RawHandle { + RawHandle(handle) + } + + fn raw(&self) -> HANDLE { self.0 } + } + + struct FindNextFileHandle(HANDLE); + + fn get_path(f: &File) -> io::Result { + fill_utf16_buf(|buf, sz| unsafe { + GetFinalPathNameByHandleW(f.handle.raw(), buf, sz, + VOLUME_NAME_DOS) + }, |buf| { + PathBuf::from(OsString::from_wide(buf)) + }) + } + + fn move_item(file: &File, ctx: &mut RmdirContext) -> io::Result<()> { + let mut tmpname = ctx.base_dir.join(format!{"rm-{}", ctx.counter}); + ctx.counter += 1; + // Try to rename the file. If it already exists, just retry with an other + // filename. + while let Err(err) = file.rename(tmpname.as_ref(), false) { + if err.kind() != io::ErrorKind::AlreadyExists { return Err(err) }; + tmpname = ctx.base_dir.join(format!("rm-{}", ctx.counter)); + ctx.counter += 1; + } + Ok(()) + } + + fn set_perm(path: &Path, perm: FilePermissions) -> io::Result<()> { + let mut opts = OpenOptions::new(); + opts.access_mode(FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES); + opts.custom_flags(FILE_FLAG_BACKUP_SEMANTICS); + let file = try!(File::open(path, &opts)); + file.set_perm(perm) + } + + const VOLUME_NAME_DOS: DWORD = 0x0; +} diff --git a/src/scripter.rs b/src/scripter.rs new file mode 100644 index 0000000..66d7ba5 --- /dev/null +++ b/src/scripter.rs @@ -0,0 +1,67 @@ +// Copyright 2017 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use std::io::Write; + +use errors::*; +use util::*; + +const TEMPLATE: &'static str = include_str!("../install-template.sh"); + + +actor!{ + #[derive(Debug)] + pub struct Scripter { + /// The name of the product, for display + product_name: String = "Product", + + /// The directory under lib/ where the manifest lives + rel_manifest_dir: String = "manifestlib", + + /// The string to print after successful installation + success_message: String = "Installed.", + + /// Places to look for legacy manifests to uninstall + legacy_manifest_dirs: String = "", + + /// The name of the output script + output_script: String = "install.sh", + } +} + +impl Scripter { + /// Generate the actual installer script + pub fn run(self) -> Result<()> { + // Replace dashes in the success message with spaces (our arg handling botches spaces) + // (TODO: still needed? kept for compatibility for now...) + let product_name = self.product_name.replace('-', " "); + + // Replace dashes in the success message with spaces (our arg handling botches spaces) + // (TODO: still needed? kept for compatibility for now...) + let success_message = self.success_message.replace('-', " "); + + let script = TEMPLATE + .replace("%%TEMPLATE_PRODUCT_NAME%%", &sh_quote(&product_name)) + .replace("%%TEMPLATE_REL_MANIFEST_DIR%%", &self.rel_manifest_dir) + .replace("%%TEMPLATE_SUCCESS_MESSAGE%%", &sh_quote(&success_message)) + .replace("%%TEMPLATE_LEGACY_MANIFEST_DIRS%%", &sh_quote(&self.legacy_manifest_dirs)) + .replace("%%TEMPLATE_RUST_INSTALLER_VERSION%%", &sh_quote(&::RUST_INSTALLER_VERSION)); + + create_new_executable(&self.output_script)? + .write_all(script.as_ref()) + .chain_err(|| format!("failed to write output script '{}'", self.output_script)) + } +} + +fn sh_quote(s: &T) -> String { + // We'll single-quote the whole thing, so first replace single-quotes with + // '"'"' (leave quoting, double-quote one `'`, re-enter single-quoting) + format!("'{}'", s.to_string().replace('\'', r#"'"'"'"#)) +} diff --git a/src/tarballer.rs b/src/tarballer.rs new file mode 100644 index 0000000..48bac68 --- /dev/null +++ b/src/tarballer.rs @@ -0,0 +1,126 @@ +// Copyright 2017 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use std::io::{self, Write}; +use std::path::Path; + +use flate2; +use flate2::write::GzEncoder; +use tar::Builder; +use walkdir::WalkDir; +use xz2::write::XzEncoder; + +use errors::*; +use util::*; + +actor!{ + #[derive(Debug)] + pub struct Tarballer { + /// The input folder to be compressed + input: String = "package", + + /// The prefix of the tarballs + output: String = "./dist", + + /// The folder in which the input is to be found + work_dir: String = "./workdir", + } +} + +impl Tarballer { + /// Generate the actual tarballs + pub fn run(self) -> Result<()> { + let tar_gz = self.output.clone() + ".tar.gz"; + let tar_xz = self.output.clone() + ".tar.xz"; + + // Remove any existing files + for file in &[&tar_gz, &tar_xz] { + if Path::new(file).exists() { + remove_file(file)?; + } + } + + // Sort files by their suffix, to group files with the same name from + // different locations (likely identical) and files with the same + // extension (likely containing similar data). + let (dirs, mut files) = get_recursive_paths(&self.work_dir, &self.input) + .chain_err(|| "failed to collect file paths")?; + files.sort_by(|a, b| a.bytes().rev().cmp(b.bytes().rev())); + + // Prepare the .tar.gz file + let gz = GzEncoder::new(create_new_file(tar_gz)?, flate2::Compression::Best); + + // Prepare the .tar.xz file + let xz = XzEncoder::new(create_new_file(tar_xz)?, 9); + + // Write the tar into both encoded files. We write all directories + // first, so files may be directly created. (see rustup.rs#1092) + let mut builder = Builder::new(Tee(gz, xz)); + for path in dirs { + let src = Path::new(&self.work_dir).join(&path); + builder.append_dir(&path, &src) + .chain_err(|| format!("failed to tar dir '{}'", src.display()))?; + } + for path in files { + let src = Path::new(&self.work_dir).join(&path); + builder.append_file(&path, &mut open_file(&src)?) + .chain_err(|| format!("failed to tar file '{}'", src.display()))?; + } + let Tee(gz, xz) = builder.into_inner() + .chain_err(|| "failed to finish writing .tar stream")?; + + // Finish both encoded files + gz.finish().chain_err(|| "failed to finish .tar.gz file")?; + xz.finish().chain_err(|| "failed to finish .tar.xz file")?; + + Ok(()) + } +} + +/// Returns all `(directories, files)` under the source path +fn get_recursive_paths(root: P, name: Q) -> Result<(Vec, Vec)> + where P: AsRef, Q: AsRef +{ + let root = root.as_ref(); + let name = name.as_ref(); + + if !name.is_relative() && !name.starts_with(root) { + bail!("input '{}' is not in work dir '{}'", name.display(), root.display()); + } + + let mut dirs = vec![]; + let mut files = vec![]; + for entry in WalkDir::new(root.join(name)) { + let entry = entry?; + let path = entry.path().strip_prefix(root)?; + let path = path_to_str(&path)?; + + if entry.file_type().is_dir() { + dirs.push(path.to_owned()); + } else { + files.push(path.to_owned()); + } + } + Ok((dirs, files)) +} + +struct Tee(A, B); + +impl Write for Tee { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.write_all(buf) + .and(self.1.write_all(buf)) + .and(Ok(buf.len())) + } + + fn flush(&mut self) -> io::Result<()> { + self.0.flush().and(self.1.flush()) + } +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..acc269a --- /dev/null +++ b/src/util.rs @@ -0,0 +1,143 @@ +// Copyright 2017 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + + +use std::fs; +use std::path::Path; +use walkdir::WalkDir; + +// Needed to set the script mode to executable. +#[cfg(unix)] +use std::os::unix::fs::OpenOptionsExt; +// FIXME: what about Windows? Are default ACLs executable? + +use errors::*; + +/// Convert a `&Path` to a UTF-8 `&str` +pub fn path_to_str(path: &Path) -> Result<&str> { + path.to_str().ok_or_else(|| { + ErrorKind::Msg(format!("path is not valid UTF-8 '{}'", path.display())).into() + }) +} + +/// Wrap `fs::copy` with a nicer error message +pub fn copy, Q: AsRef>(from: P, to: Q) -> Result { + fs::copy(&from, &to) + .chain_err(|| format!("failed to copy '{}' to '{}'", + from.as_ref().display(), to.as_ref().display())) +} + +/// Wrap `fs::create_dir` with a nicer error message +pub fn create_dir>(path: P) -> Result<()> { + fs::create_dir(&path) + .chain_err(|| format!("failed to create dir '{}'", path.as_ref().display())) +} + +/// Wrap `fs::create_dir_all` with a nicer error message +pub fn create_dir_all>(path: P) -> Result<()> { + fs::create_dir_all(&path) + .chain_err(|| format!("failed to create dir '{}'", path.as_ref().display())) +} + +/// Wrap `fs::OpenOptions::create_new().open()` as executable, with a nicer error message +pub fn create_new_executable>(path: P) -> Result { + let mut options = fs::OpenOptions::new(); + options.write(true).create_new(true); + #[cfg(unix)] options.mode(0o755); + options.open(&path) + .chain_err(|| format!("failed to create file '{}'", path.as_ref().display())) +} + +/// Wrap `fs::OpenOptions::create_new().open()`, with a nicer error message +pub fn create_new_file>(path: P) -> Result { + fs::OpenOptions::new().write(true).create_new(true).open(&path) + .chain_err(|| format!("failed to create file '{}'", path.as_ref().display())) +} + +/// Wrap `fs::File::open()` with a nicer error message +pub fn open_file>(path: P) -> Result { + fs::File::open(&path) + .chain_err(|| format!("failed to open file '{}'", path.as_ref().display())) +} + +/// Wrap `remove_dir_all` with a nicer error message +pub fn remove_dir_all>(path: P) -> Result<()> { + ::remove_dir_all::remove_dir_all(path.as_ref()) + .chain_err(|| format!("failed to remove dir '{}'", path.as_ref().display())) +} + +/// Wrap `fs::remove_file` with a nicer error message +pub fn remove_file>(path: P) -> Result<()> { + fs::remove_file(path.as_ref()) + .chain_err(|| format!("failed to remove file '{}'", path.as_ref().display())) +} + +/// Wrap `fs::rename` with a nicer error message +pub fn rename, Q: AsRef>(from: P, to: Q) -> Result<()> { + fs::rename(&from, &to) + .chain_err(|| format!("failed to rename '{}' to '{}'", + from.as_ref().display(), to.as_ref().display())) +} + +/// Copies the `src` directory recursively to `dst`. Both are assumed to exist +/// when this function is called. +pub fn copy_recursive(src: &Path, dst: &Path) -> Result<()> { + copy_with_callback(src, dst, |_, _| Ok(())) +} + +/// Copies the `src` directory recursively to `dst`. Both are assumed to exist +/// when this function is called. Invokes a callback for each path visited. +pub fn copy_with_callback(src: &Path, dst: &Path, mut callback: F) -> Result<()> + where F: FnMut(&Path, fs::FileType) -> Result<()> +{ + for entry in WalkDir::new(src).min_depth(1) { + let entry = entry?; + let file_type = entry.file_type(); + let path = entry.path().strip_prefix(src)?; + let dst = dst.join(path); + + if file_type.is_dir() { + create_dir(&dst)?; + } else { + copy(entry.path(), dst)?; + } + callback(&path, file_type)?; + } + Ok(()) +} + + +/// Create an "actor" with default values and setters for all fields. +macro_rules! actor { + ($( #[ $attr:meta ] )+ pub struct $name:ident { + $( $( #[ $field_attr:meta ] )+ $field:ident : $type:ty = $default:expr, )* + }) => { + $( #[ $attr ] )+ + pub struct $name { + $( $( #[ $field_attr ] )+ $field : $type, )* + } + + impl Default for $name { + fn default() -> Self { + $name { + $( $field : $default.into(), )* + } + } + } + + impl $name { + $( $( #[ $field_attr ] )+ + pub fn $field>(&mut self, value: T) -> &mut Self { + self.$field = value.into(); + self + })+ + } + } +}