diff options
-rw-r--r-- | .gitignore | 5 | ||||
-rw-r--r-- | Cargo.toml | 8 | ||||
-rwxr-xr-x | gen-installer.sh | 322 | ||||
-rw-r--r-- | src/generator.rs | 211 | ||||
-rw-r--r-- | src/lib.rs | 21 | ||||
-rw-r--r-- | src/main.rs | 58 | ||||
-rw-r--r-- | src/main.yml | 62 |
7 files changed, 365 insertions, 322 deletions
@@ -1,2 +1,5 @@ *~ -tmp
\ No newline at end of file +tmp +target/ +**/*.rs.bk +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ef1656f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +authors = ["The Rust Project Developers"] +name = "installer" +version = "0.0.0" + +[dependencies.clap] +version = "2.22.1" +features = ["yaml"] 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="<none>" - 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/src/generator.rs b/src/generator.rs new file mode 100644 index 0000000..ff2cf3a --- /dev/null +++ b/src/generator.rs @@ -0,0 +1,211 @@ +// 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 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use std::fs; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; +use std::process::Command; + +#[derive(Debug)] +pub struct Generator { + product_name: String, + component_name: String, + package_name: String, + rel_manifest_dir: String, + success_message: String, + legacy_manifest_dirs: String, + non_installed_overlay: String, + bulk_dirs: String, + image_dir: String, + work_dir: String, + output_dir: String, +} + +impl Default for Generator { + fn default() -> Generator { + Generator { + product_name: "Product".into(), + component_name: "component".into(), + package_name: "package".into(), + rel_manifest_dir: "packagelib".into(), + success_message: "Installed.".into(), + legacy_manifest_dirs: "".into(), + non_installed_overlay: "".into(), + bulk_dirs: "".into(), + image_dir: "./install_image".into(), + work_dir: "./workdir".into(), + output_dir: "./dist".into(), + } + } +} + +impl Generator { + /// The name of the product, for display + pub fn product_name(&mut self, value: String) -> &mut Self { + self.product_name = value; + self + } + + /// The name of the component, distinct from other installed components + pub fn component_name(&mut self, value: String) -> &mut Self { + self.component_name = value; + self + } + + /// The name of the package, tarball + pub fn package_name(&mut self, value: String) -> &mut Self { + self.package_name = value; + self + } + + /// The directory under lib/ where the manifest lives + pub fn rel_manifest_dir(&mut self, value: String) -> &mut Self { + self.rel_manifest_dir = value; + self + } + + /// The string to print after successful installation + pub fn success_message(&mut self, value: String) -> &mut Self { + self.success_message = value; + self + } + + /// Places to look for legacy manifests to uninstall + pub fn legacy_manifest_dirs(&mut self, value: String) -> &mut Self { + self.legacy_manifest_dirs = value; + self + } + + /// Directory containing files that should not be installed + pub fn non_installed_overlay(&mut self, value: String) -> &mut Self { + self.non_installed_overlay = value; + self + } + + /// Path prefixes of directories that should be installed/uninstalled in bulk + pub fn bulk_dirs(&mut self, value: String) -> &mut Self { + self.bulk_dirs = value; + self + } + + /// The directory containing the installation medium + pub fn image_dir(&mut self, value: String) -> &mut Self { + self.image_dir = value; + self + } + + /// The directory to do temporary work + pub fn work_dir(&mut self, value: String) -> &mut Self { + self.work_dir = value; + self + } + + /// The location to put the final image and tarball + pub fn output_dir(&mut self, value: String) -> &mut Self { + self.output_dir = value; + self + } + + /// Generate the actual installer tarball + pub fn run(self) -> io::Result<()> { + let src_dir = Path::new(::SOURCE_DIRECTORY); + fs::read_dir(&src_dir)?; + + fs::create_dir_all(&self.work_dir)?; + + let package_dir = Path::new(&self.work_dir).join(&self.package_name); + if package_dir.exists() { + fs::remove_dir_all(&package_dir)?; + } + + let component_dir = package_dir.join(&self.component_name); + fs::create_dir_all(&component_dir)?; + let mut files = cp_r(self.image_dir.as_ref(), &component_dir)?; + + // Filter out files that are covered by bulk dirs. + let bulk_dirs: Vec<_> = self.bulk_dirs.split(',').filter(|s| !s.is_empty()).collect(); + files.retain(|f| !bulk_dirs.iter().any(|d| f.starts_with(d))); + + // Write the manifest + let manifest = fs::File::create(component_dir.join("manifest.in"))?; + for file in files { + writeln!(&manifest, "file:{}", file.display())?; + } + for dir in bulk_dirs { + writeln!(&manifest, "dir:{}", dir)?; + } + drop(manifest); + + // Write the component name + let components = fs::File::create(package_dir.join("components"))?; + writeln!(&components, "{}", self.component_name)?; + drop(components); + + // Write the installer version (only used by combine-installers.sh) + let version = fs::File::create(package_dir.join("rust-installer-version"))?; + writeln!(&version, "{}", ::RUST_INSTALLER_VERSION)?; + drop(version); + + // Copy the overlay + if !self.non_installed_overlay.is_empty() { + cp_r(self.non_installed_overlay.as_ref(), &package_dir)?; + } + + // Generate the install script (TODO: run this in-process!) + let output_script = package_dir.join("install.sh"); + let status = Command::new(src_dir.join("gen-install-script.sh")) + .arg(format!("--product-name={}", self.product_name)) + .arg(format!("--rel-manifest-dir={}", self.rel_manifest_dir)) + .arg(format!("--success-message={}", self.success_message)) + .arg(format!("--legacy-manifest-dirs={}", self.legacy_manifest_dirs)) + .arg(format!("--output-script={}", output_script.display())) + .status()?; + if !status.success() { + let msg = format!("failed to generate install script: {}", status); + return Err(io::Error::new(io::ErrorKind::Other, msg)); + } + + // Make the tarballs (TODO: run this in-process!) + fs::create_dir_all(&self.output_dir)?; + let output = Path::new(&self.output_dir).join(&self.package_name); + let status = Command::new(src_dir.join("make-tarballs.sh")) + .arg(format!("--work-dir={}", self.work_dir)) + .arg(format!("--input={}", self.package_name)) + .arg(format!("--output={}", output.display())) + .status()?; + if !status.success() { + let msg = format!("failed to make tarballs: {}", status); + return Err(io::Error::new(io::ErrorKind::Other, msg)); + } + + Ok(()) + } +} + +/// Copies the `src` directory recursively to `dst`. Both are assumed to exist +/// when this function is called. Returns a list of files written relative to `dst`. +pub fn cp_r(src: &Path, dst: &Path) -> io::Result<Vec<PathBuf>> { + let mut files = vec![]; + for f in fs::read_dir(src)? { + let f = f?; + let path = f.path(); + let name = PathBuf::from(f.file_name()); + let dst = dst.join(&name); + if f.file_type()?.is_dir() { + fs::create_dir(&dst)?; + let subfiles = cp_r(&path, &dst)?; + files.extend(subfiles.into_iter().map(|f| name.join(f))); + } else { + fs::copy(&path, &dst)?; + files.push(name); + } + } + Ok(files) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..c81dc12 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,21 @@ +// 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 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +mod generator; + +pub use generator::Generator; + +/// The base source directory +/// (FIXME: statically compiling this means we can't ship installer binaries!) +const SOURCE_DIRECTORY: &'static str = env!("CARGO_MANIFEST_DIR"); + +/// 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..bdc2dd5 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,58 @@ +#[macro_use] +extern crate clap; +extern crate installer; + +use clap::{App, ArgMatches}; +use installer::Generator; + +fn main() { + let yaml = load_yaml!("main.yml"); + let matches = App::from_yaml(yaml).get_matches(); + + match matches.subcommand() { + ("generate", Some(matches)) => generate(matches), + _ => unreachable!(), + } +} + +fn generate(matches: &ArgMatches) { + let mut gen = Generator::default(); + matches + .value_of("product-name") + .map(|s| gen.product_name(s.into())); + matches + .value_of("component-name") + .map(|s| gen.component_name(s.into())); + matches + .value_of("package-name") + .map(|s| gen.package_name(s.into())); + matches + .value_of("rel-manifest-dir") + .map(|s| gen.rel_manifest_dir(s.into())); + matches + .value_of("success-message") + .map(|s| gen.success_message(s.into())); + matches + .value_of("legacy-manifest-dirs") + .map(|s| gen.legacy_manifest_dirs(s.into())); + matches + .value_of("non-installed-overlay") + .map(|s| gen.non_installed_overlay(s.into())); + matches + .value_of("bulk-dirs") + .map(|s| gen.bulk_dirs(s.into())); + matches + .value_of("image-dir") + .map(|s| gen.image_dir(s.into())); + matches + .value_of("work-dir") + .map(|s| gen.work_dir(s.into())); + matches + .value_of("output-dir") + .map(|s| gen.output_dir(s.into())); + + if let Err(e) = gen.run() { + println!("failed to generate installer: {}", e); + std::process::exit(1); + } +} diff --git a/src/main.yml b/src/main.yml new file mode 100644 index 0000000..ead98b6 --- /dev/null +++ b/src/main.yml @@ -0,0 +1,62 @@ +name: installer +settings: + - ArgRequiredElseHelp +subcommands: + - generate: + 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 + |