Artifact [42f6088384]
Not logged in

Artifact 42f6088384561b0d8bff1a19423b167c16d3ba78:


###############################################################################
#
# pkgr_upload.eagle --
#
# Extensible Adaptable Generalized Logic Engine (Eagle)
# Package Repository Client (Upload Tool)
#
# Copyright (c) 2007-2012 by Joe Mistachkin.  All rights reserved.
#
# See the file "license.terms" for information on usage and redistribution of
# this file, and for a DISCLAIMER OF ALL WARRANTIES.
#
# RCS: @(#) $Id: $
#
###############################################################################

#
# NOTE: Use our own namespace here because even though we do not directly
#       support namespaces ourselves, we do not want to pollute the global
#       namespace if this script actually ends up being evaluated in Tcl.
#
namespace eval ::PackageUploader {
  #
  # NOTE: This procedure is used to report errors that prevent this tool
  #       from running to completion (e.g. invalid command line arguments,
  #       etc).  It may be used to report a specific error.  It will always
  #       emit the command line usage information.
  #
  proc usage { {error ""} } {
    if {[string length $error] > 0} then {puts stdout $error}

    puts stdout "usage:\
  [file tail [info nameofexecutable]]\
  [file tail [info script]] \[apiKey\] \[name\] \[version\] \[language\]\
  \[fileName1\] ... \[fileNameN\]"

    exit 1
  }

  #
  # NOTE: This procedure sets up the default values for all configuration
  #       parameters used by the package uploader client.  The script
  #       argument is the fully qualified path and file name for the script
  #       being evaluated.
  #
  proc setupUploadVars { script } {
    #
    # NOTE: What is the fully qualified path to the directory containing the
    #       checkout for the package client?
    #
    variable checkoutDirectory

    if {![info exists checkoutDirectory]} then {
      set checkoutDirectory [file dirname $script]
    }

    #
    # NOTE: The command to use when attempting to stage package files using
    #       Fossil.
    #
    variable fossilAddCommand; # DEFAULT fossil add {${fileName}}

    if {![info exists fossilAddCommand]} then {
      set fossilAddCommand {fossil add {${fileName}}}
    }

    #
    # NOTE: The command to use when attempting to commit package files using
    #       Fossil.
    #
    variable fossilCommitCommand; # DEFAULT fossil commit ...

    if {![info exists fossilCommitCommand]} then {
      set fossilCommitCommand {fossil commit -m {${comment}}\
          --branch {${branch}} --user anonymous --chdir \
          {${checkoutDirectory}} --no-prompt}
    }

    #
    # NOTE: The regular expression pattern used when attempting to verify
    #       that Fossil committed a set of files.
    #
    variable fossilCommitPattern; # DEFAULT: {^New_Version: ([0-9a-f]{40})$}

    if {![info exists fossilCommitPattern]} then {
      set fossilCommitPattern {^New_Version: ([0-9a-f]{40})$}
    }
  }

  #
  # NOTE: This procedure returns a string value, formatted for use within a
  #       script block being processed by the [string map] command.  The
  #       value argument is the string value to format.  No return value is
  #       reserved to indicate an error.  This procedure may not raise any
  #       script errors.
  #
  proc formatStringMapValue { value } {
    if {[string length $value] == 0} then {
      return \"\"
    }

    set list [list $value]

    if {$value eq $list} then {
      return $value
    } else {
      return $list
    }
  }

  #
  # NOTE: This procedure processes a list of qualified file names and tries
  #       to determine their common containing directory, if any.  The
  #       fileNames argument is the list of (fully?) qualified file names to
  #       process.
  #
  proc getContainingDirectory { fileNames } {
    set result ""
    set resultParts [list]

    foreach fileName $fileNames {
      set directory [file dirname $fileName]
      set directoryParts [file split $directory]

      if {[llength $resultParts] == 0 || \
          [llength $directoryParts] < [llength $resultParts]} then {
        set result $directory
        set resultParts $directoryParts
      } elseif {[llength $directoryParts] == [llength $resultParts] && \
          $directory ne $result} then {
        set result [file dirname $directory]
        set resultParts [file split $result]
      }
    }

    return $result
  }

  #
  # NOTE: This procedure attempts to process a list of (fully?) qualified file
  #       names and return the corresponding list of relative file names.  The
  #       fileNames argument is the list of (fully?) qualified file names to
  #       process.  This procedure may raise script errors.
  #
  proc getRelativeFileNames { fileNames } {
    set directory [getContainingDirectory $fileNames]
    set directoryParts [file split $directory]
    set fileNameIndex [expr {[llength $directoryParts] - 1}]

    if {$fileNameIndex < 0} then {
      error [appendArgs \
          "invalid containing directory \"" $directory \
          "\": cannot go up one level"]
    }

    set relativeFileNames [list]

    foreach fileName $fileNames {
      set fileNameParts [lrange \
          [file split $fileName] $fileNameIndex end]

      if {$maximumLevels > 0 && \
          [llength $fileNameParts] > $maximumLevels} then {
        error [appendArgs \
            "depth for file name \"" $fileName \
            "\" exceeds maximum (" $maximumLevels )]
      }

      set relativeFileName [eval file join $fileNameParts]

      if {[string length $relativeFileName] == 0 || \
          [file pathtype $relativeFileName] ne "relative"} then {
        error [appendArgs \
            "bad file name \"" $relativeFileName "\", not relative"]
      }

      lappend relativeFileNames $relativeFileName
    }

    return $relativeFileNames
  }

  #
  # NOTE: This procedure attempts to create a script chunk that appends the
  #       specified list of file names to a list variable.  The fileNames
  #       argument is the list of (fully?) qualified file names to append to
  #       the list variable.  The maximumLevels argument is the maximum path
  #       depth allowed for all file names.  This procedure may raise script
  #       errors.
  #
  proc getScriptChunkForFileNames { fileNames maximumLevels } {
    set result ""
    set relativeFileNames [getRelativeFileNames $fileNames]

    foreach relativeFileName $relativeFileNames {
      if {[string length $result] > 0} then {
        append result \n
      }

      append result {  lappend fileNames [file join }
      append result [file split $relativeFileName]
      append result \]
    }

    return $result
  }

  #
  # NOTE: This procedure creates and returns a script block designed for use
  #       with the package repository server in order to download and provide
  #       a package consisting of a set of files.  The language argument must
  #       be the literal string "eagle" or the literal string "tcl".  The
  #       version argument must be one of the literal strings "8.4", "8.5",
  #       or "8.6" when the language is "tcl" -OR- the literal string "1.0"
  #       when the language is "eagle".  The platform argument must be an
  #       empty string -OR- one of the literal strings "neutral", "win32-arm",
  #       "win32-x86", "win64-arm64", "win64-ia64", or "win64-x64".  The
  #       fileNames argument is the list of (fully?) qualified file names to
  #       be downloaded when the associated package is being provided.  The
  #       options argument is reserved for future use, it should be an empty
  #       list.
  #
  proc createRepositoryScript { language version platform fileNames options } {
    ::PackageDownloader::verifyLanguageAndVersion $language $version isClient
    ::PackageDownloader::verifyPlatform $platform platform

    return [string trim [string map [list \r\n \n \
        %language% [formatStringMapValue $language] \
        %version% [formatStringMapValue $version] \
        %platform% [formatStringMapValue $platform] \
        %backslash% \\ %ns% ::PackageDownloader %fileNames% \
        [getScriptChunkForFileNames \
        $fileNames 2]] {
apply [list [list] {
  package require Eagle.Package.Downloader

  %ns%::resetCookieAndLoginSimple

  set fileNames [list]

%fileNames%

  set options [list %backslash%
      -persistent false -usePgp true -useAutoPath true]

  %ns%::downloadFiles %language% %version% %platform% $fileNames $options
  %ns%::logoutAndResetCookie
}]
    }]]
  }

  #
  # NOTE: This procedure attempts to stage the specified package files using
  #       Fossil.  The fileNames argument is a list of (fully?) qualified
  #       local file names to stage.
  #
  proc stagePackageFiles { language version platform fileNames } {
    variable checkoutDirectory
    variable fossilAddCommand

    set relativeFileNames [getRelativeFileNames $fileNames]
    set savedPwd [pwd]; cd $checkoutDirectory

    foreach fileName $fileNames relativeFileName $relativeFileNames {
      file mkdir [file join \
          $language $version $platform [file dirname $relativeFileName]]

      file copy $fileName $relativeFileName
      set fileName $relativeFileName

      if {[isEagle]} then {
        set fileName [::PackageRepository::formatExecArgument $fileName]

        if {[catch {
          eval exec -success Success [subst $fossilAddCommand]
        } error]} then {
          cd $savedPwd

          error [appendArgs \
              "failed to stage file \"" $fileName "\": " $error]
        }
      } else {
        if {[catch {
          eval exec [subst $fossilAddCommand]
        } error]} then {
          cd $savedPwd

          error [appendArgs \
              "failed to stage file \"" $fileName "\": " $error]
        }
      }
    }

    cd $savedPwd
  }

  #
  # NOTE: This procedure attempts to commit the staged package files to the
  #       remote package file repository using Fossil.  The varName argument
  #       is the name of a scalar variable in the context of the immediate
  #       caller that will receive the resulting Fossil check-in identifier.
  #
  proc commitPackageFiles { varName } {
    variable checkoutDirectory
    variable fossilCommitCommand
    variable fossilCommitPattern

    set branch ""; # TODO: Figure out a good branch.
    set comment ""; # TODO: Figure out a good comment.

    if {[isEagle]} then {
      if {[catch {
        eval exec -nocarriagereturns -stdout output -stderr error \
            [subst $fossilCommitCommand]
      } result] == 0} then {
        set result [appendArgs $output $error]
      } else {
        return false
      }
    } else {
      if {[catch {
        eval exec [subst $fossilCommitCommand]
      } result]} then {
        return false
      }
    }

    if {[string length $varName] > 0} then {
      upvar 1 $varName checkin
    }

    if {![info exists result] || \
        ![regexp -line -- $fossilCommitPattern $result dummy checkin]} then {
      return false
    }

    return true
  }

  #
  # NOTE: Figure out the fully qualified path to the current script file.
  #       If necessary, add it to the auto-path for the interpreter.  The
  #       necessary supporting packages (i.e. the Package Repository and
  #       other support packages) that are assumed to exist in the same
  #       directory as the current script file.
  #
  variable pkgr_path; # DEFAULT: <unset>

  if {![info exists pkgr_path]} then {
    set pkgr_path [file normalize [file dirname [info script]]]

    if {![info exists ::auto_path] || \
        [lsearch -exact $::auto_path $pkgr_path] == -1} then {
      lappend ::auto_path $pkgr_path
    }
  }

  #
  # NOTE: *TODO* Pre-create the namespace for the Package Repository Client
  #       package and then forcibly adjust various settings to the values
  #       necessary for this tool.  In the future, this section may need to
  #       be tweaked to account for changes to the Package Repository Client
  #       package.
  #
  namespace eval ::PackageRepository {
    variable verboseUriDownload true
  }

  #
  # NOTE: This package requires both the package repository and downloader
  #       client packages.
  #
  package require Eagle.Package.Downloader

  #
  # NOTE: This package requires that support for namespaces, which is an
  #       optional feature of Eagle, must be enabled.
  #
  if {[isEagle] && ![namespace enable]} then {
    error "namespaces must be enabled for this package"
  }

  #
  # NOTE: Attempt to read optional settings file now.  This may override
  #       one or more of the variable setup in the next step.
  #
  ::PackageRepository::maybeReadSettingsFile [info script]

  #
  # NOTE: Setup the variables, within this namespace, used by this script.
  #
  setupUploadVars [info script]

  #
  # NOTE: Provide the package to the interpreter.
  #
  package provide Eagle.Package.Uploader \
    [expr {[isEagle] ? [info engine PatchLevel] : "1.0"}]

  #
  # NOTE: Verify that the number of command line arguments meets the basic
  #       requirements of this tool.
  #
  if {[info exists ::argv] && [llength $::argv] >= 5} then {
    #
    # NOTE: All the necessary arguments were supplied on the command line,
    #       use batch mode.
    #
  } else {
    #
    # NOTE: One or more of the necessary arguments were not supplied on the
    #       command line, use interactive mode.  This will create a graphical
    #       user interface, using Tk or WinForms.  If any of the necessary
    #       arguments were supplied on the command line, they will be used to
    #       populate those fields on the graphical user interface.
    #
    if {[isEagle]} then {

    } else {

    }
  }
}