###############################################################################
#
# pkgd.eagle --
#
# Extensible Adaptable Generalized Logic Engine (Eagle)
# Package Downloader Client
#
# 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 ::PackageDownloader {
#
# NOTE: This procedure sets up the default values for all URN configuration
# parameters used by the package downloader client. If the force
# argument is non-zero, any existing values will be overwritten and
# set back to their default values.
#
proc setupDownloadUrnVars { force } {
#
# NOTE: The URN, relative to the base URI, where a login request may
# be sent.
#
variable loginUrn; # DEFAULT: pkgd_login
if {$force || ![info exists loginUrn]} then {
set loginUrn pkgd_login
}
#
# NOTE: The URN, relative to the base URI, where the list of supported
# platforms for a single package may be found.
#
variable platformsUrn; # DEFAULT: pkgd_platforms
if {$force || ![info exists platformsUrn]} then {
set platformsUrn pkgd_platforms
}
#
# NOTE: The URN, relative to the base URI, where a single package file
# may be found.
#
variable downloadUrn; # DEFAULT: pkgd_file
if {$force || ![info exists downloadUrn]} then {
set downloadUrn pkgd_file
}
#
# NOTE: The URN, relative to the base URI, where a logout request may
# be sent.
#
variable logoutUrn; # DEFAULT: pkgd_logout
if {$force || ![info exists logoutUrn]} then {
set logoutUrn pkgd_logout
}
}
#
# NOTE: This procedure sets up the default values for all URI configuration
# parameters used by the package downloader client. If the force
# argument is non-zero, any existing values will be overwritten and
# set back to their default values.
#
proc setupDownloadUriVars { force } {
#
# NOTE: The base URI used to build the URIs for the package file server.
#
variable baseUri; # DEFAULT: https://urn.to/r
if {$force || ![info exists baseUri]} then {
set baseUri https://urn.to/r
}
#
# NOTE: The URI where a login request may be sent. This should return a
# payload containing the necessary HTTP(S) cookie information.
#
variable loginUri; # DEFAULT: ${baseUri}/${loginUrn}?...
if {$force || ![info exists loginUri]} then {
set loginUri [appendArgs \
{${baseUri}/${loginUrn}?} {[uriEscape name $userName]} & \
{[uriEscape password $password]}]
}
#
# NOTE: The URI where the list of supported platforms for a single
# package may be found.
#
variable platformsUri; # DEFAULT: ${baseUri}/${platformsUrn}?...
if {$force || ![info exists platformsUri]} then {
set platformsUri {${baseUri}/${platformsUrn}?download&name=trunk}
}
#
# NOTE: The URI where a single package file may be found. This file will
# belong to a specific version of one package.
#
variable downloadUri; # DEFAULT: ${baseUri}/${downloadUrn}?...
if {$force || ![info exists downloadUri]} then {
set downloadUri [appendArgs \
{${baseUri}/${downloadUrn}?download&ci=trunk&} \
{[uriEscape filename $fileName]}]
}
#
# NOTE: The URI where a logout request should be sent. This should
# return a payload indicating that the logout was successful.
#
variable logoutUri; # DEFAULT: ${baseUri}/${logoutUrn}?...
if {$force || ![info exists logoutUri]} then {
set logoutUri [appendArgs \
{${baseUri}/${logoutUrn}?} {[uriEscape authToken $authToken]}]
}
}
#
# NOTE: This procedure sets up the default values for all configuration
# parameters used by the package downloader client. The script
# argument is the fully qualified path and file name for the script
# being evaluated.
#
proc setupDownloadVars { script } {
#
# NOTE: What is the fully qualified path to the directory containing the
# package downloader client?
#
variable clientDirectory
if {![info exists clientDirectory]} then {
set clientDirectory [file dirname $script]
}
#
# NOTE: This is the HTTP(S) login cookie to use when downloading files
# from the package file server.
#
variable loginCookie; # DEFAULT: NONE
if {![info exists loginCookie]} then {
set loginCookie [list]
}
#
# NOTE: Prevent progress messages from being displayed while downloading
# from the repository, etc? By default, this is enabled.
#
variable quiet; # DEFAULT: true
if {![info exists quiet]} then {
set quiet true
}
#
# NOTE: The user name for the public account on the package file server.
# If this is an empty string, there is no public account.
#
variable publicUserName; # DEFAULT: public
if {![info exists publicUserName]} then {
set publicUserName public
}
#
# NOTE: The password associated with the public account on the package
# file server. If this is an empty string, the public account is
# disabled. This is not considered to be a secret; however, it
# should not be shared with any person or organization that does
# not have access to the package downloader client.
#
variable publicPassword; # DEFAULT: X+NlF2obS5tQFKIsf/q345/naqVSGD67Cg
if {![info exists publicPassword]} then {
set publicPassword X+NlF2obS5tQFKIsf/q345/naqVSGD67Cg
}
#
# NOTE: The root directory where any persistent packages will be saved.
#
variable persistentRootDirectory; # DEFAULT: [getPersistentRootDirectory]
if {![info exists persistentRootDirectory]} then {
set persistentRootDirectory [getPersistentRootDirectory]
}
#
# NOTE: The root directory where any temporary packages will be written.
#
variable temporaryRootDirectory; # DEFAULT: [getFileTempDirectory PKGD_TEMP]
if {![info exists temporaryRootDirectory]} then {
set temporaryRootDirectory \
[::PackageRepository::getFileTempDirectory PKGD_TEMP]
}
#
# NOTE: Is this package being run by the package installer tool? If so,
# all downloaded packages should be automatically persisted to the
# library path.
#
variable viaInstall; # DEFAULT: false
if {![info exists viaInstall]} then {
set viaInstall false
}
}
#
# NOTE: This procedure modifies the URN variables used by the package
# downloader client so that one or more alternative (private?)
# backend file servers may be used. The serverId argument must
# consist only of alphanumeric characters and it must begin with
# a letter.
#
# <public>
proc useServerId { {serverId ""} } {
variable downloadUrn
variable loginUrn
variable logoutUrn
if {[string length $serverId] > 0 && \
![regexp -nocase -- {^[A-Z][0-9A-Z]*$} $serverId]} then {
error "server Id must be alphanumeric and start with a letter"
}
if {[string length $serverId] > 0} then {
#
# NOTE: Set the URN variables to values that should cause the
# specified server Id to be used (assume the server Id
# itself is valid and active).
#
set loginUrn [appendArgs pkgd_login_ $serverId]
set downloadUrn [appendArgs pkgd_file_ $serverId]
set logoutUrn [appendArgs pkgd_logout_ $serverId]
} else {
#
# NOTE: Forcibly reset URN variables to their default values.
#
setupDownloadUrnVars true
}
}
#
# NOTE: This procedure escapes a single name/value pair for use in a URI
# query string. The name argument is the name of the parameter.
# The value argument is the value of the parameter.
#
proc uriEscape { name value } {
if {[isEagle]} then {
return [appendArgs \
[uri escape data $name] = [uri escape data $value]]
} else {
package require http 2.0
return [::http::formatQuery $name $value]
}
}
#
# NOTE: This procedure returns the root directory where any packages that
# are downloaded should be saved to permanent storage for subsequent
# use. There are no arguments.
#
proc getPersistentRootDirectory {} {
global env
#
# NOTE: Allow the persistent root directory to be overridden via the
# environment. Typically, this customization will only be needed
# if multiple instances of Tcl need to share packages.
#
if {[info exists env(PKGD_ROOT)]} then {
return $env(PKGD_ROOT)
}
#
# NOTE: Fallback to returning a directory parallel to the one containing
# the library directory.
#
return [file join [file dirname [info library]] pkgd]
}
#
# NOTE: This procedure checks the configured persistent root directory for
# downloaded packages. If any checks fail, a script error is raised.
# There are no arguments. The return value is undefined.
#
proc verifyPersistentRootDirectory {} {
variable persistentRootDirectory
if {![info exists persistentRootDirectory]} then {
error "persistent root directory not set"
}
if {[string length $persistentRootDirectory] == 0} then {
error "persistent root directory is invalid"
}
#
# NOTE: Either the persistent root directory must already exist -OR- we
# must be able to create it.
#
if {![file isdirectory $persistentRootDirectory] && \
[catch {file mkdir $persistentRootDirectory}]} then {
error "persistent root directory does not exist and could not be created"
}
}
#
# NOTE: This procedure returns the name of the package index file for the
# language specified by the language argument. An empty string will
# be returned if the language is unsupported or unrecognized.
#
proc getPackageIndexFileName { language } {
if {[string length $language] == 0 || $language eq "eagle"} then {
return pkgIndex.eagle
} elseif {$language eq "tcl"} then {
return pkgIndex.tcl
} else {
return ""
}
}
#
# NOTE: This procedure, which is only used for native Tcl, generates a
# "master" package index file (i.e. "pkgIndex.tcl") suitable for
# use with native Tcl 8.4 (or higher). It will recursively scan
# for all other native Tcl package index files that are within the
# configured persistent root directory and [source] them, thereby
# causing all packages located within them to become available.
# Since Eagle (by default) already performs recursive searches for
# its package index files, this procedure is not needed for Eagle.
# The return value is undefined.
#
proc maybeCreateMasterTclPackageIndex {} {
variable persistentRootDirectory
verifyPersistentRootDirectory
set persistentDirectory $persistentRootDirectory
set fileName [file join $persistentDirectory pkgIndex.tcl]
if {[file exists $fileName]} then {return ""}
writeFile $fileName [string trim [string map [list \r\n \n] {
###############################################################################
#
# pkgIndex.tcl --
#
# Extensible Adaptable Generalized Logic Engine (Eagle)
# Generated Recursive Package Index File -- PLEASE DO NOT EDIT
#
# 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: $
#
###############################################################################
if {![package vsatisfies [package provide Tcl] 8.4]} {return}
if {[string length [package provide Eagle]] > 0} then {return}
set pkgd(savedDir) $dir; set pkgd(dirs) [list $pkgd(savedDir)]
for {set pkgd(i) 0} {$pkgd(i) < [llength $pkgd(dirs)]} {incr pkgd(i)} {
set pkgd(dir) [lindex $pkgd(dirs) $pkgd(i)]
if {$pkgd(i) > 0} then {
set pkgd(file) [file join $pkgd(dir) pkgIndex.tcl]
if {[file exists $pkgd(file)]} then {
set dir $pkgd(dir); source $pkgd(file)
}
}
eval lappend pkgd(dirs) \
[glob -nocomplain -types {d} [file join $pkgd(dir) *]]
}
set dir $pkgd(savedDir); unset -nocomplain pkgd
}]]
return ""
}
#
# NOTE: This procedure returns non-zero if the specified file seems to be
# an OpenPGP signature file. The fileName argument is the name of
# the file to check, which may or may not exist. The nameOnly
# argument should be non-zero to ignore the contents of the file.
#
proc isOpenPgpSignatureFileName { fileName nameOnly } {
if {[string length $fileName] == 0} then {
return false
}
set extension [file extension $fileName]
if {$extension eq ".txt" || $extension eq ".asc"} then {
if {!$nameOnly && [file exists $fileName]} then {
return [::PackageRepository::isOpenPgpSignature \
[readFile $fileName]]
} else {
return true
}
} else {
return false
}
}
#
# NOTE: This procedure returns non-zero if the specified file seems to be
# a Harpy script certificate file. The fileName argument is the name
# of the file to check, which may or may not exist. The nameOnly
# argument should be non-zero to ignore the contents of the file.
#
# <notUsed>
proc isHarpyCertificateFileName { fileName nameOnly } {
if {[string length $fileName] == 0} then {
return false
}
set extension [file extension $fileName]
if {$extension eq ".harpy"} then {
if {!$nameOnly && [file exists $fileName]} then {
return [::PackageRepository::isHarpyCertificate \
[readFile $fileName]]
} else {
return true
}
} else {
return false
}
}
#
# NOTE: This procedure returns the auto-path for the language specified by
# the language argument. An empty list is returned if the auto-path
# does not exist in the target language. This procedure may raise
# script errors.
#
proc getAutoPath { language } {
if {[string length $language] == 0 || $language eq "eagle"} then {
if {[isEagle]} then {
if {![info exists ::auto_path]} then {
return [list]
}
return $::auto_path
} else {
::PackageRepository::eagleMustBeReady
eagle {
if {![info exists ::auto_path]} then {
return [list]
}
return $::auto_path
}
}
} elseif {$language eq "tcl"} then {
if {[isEagle]} then {
tcl eval [tcl master] {
if {![info exists ::auto_path]} then {
return [list]
}
return $::auto_path
}
} else {
if {![info exists ::auto_path]} then {
return [list]
}
return $::auto_path
}
} else {
error "unsupported language, no idea how to query auto-path"
}
}
#
# NOTE: This procedure adds a directory to the auto-path of the specified
# language (i.e. native Tcl or Eagle). The directory will not be
# added if it is already present. The language argument must be the
# literal string "eagle" or the literal string "tcl". The directory
# argument is the fully qualified path for the directory to add to
# the auto-path.
#
proc addToAutoPath { language directory } {
#
# NOTE: Add the specified directory to the auto-path if not already
# present.
#
if {[string length $language] == 0 || $language eq "eagle"} then {
if {[isEagle]} then {
if {![info exists ::auto_path] || \
[lsearch -exact $::auto_path $directory] == -1} then {
lappend ::auto_path $directory
}
} else {
::PackageRepository::eagleMustBeReady
eagle [string map [list %directory% $directory] {
if {![info exists ::auto_path] || \
[lsearch -exact $::auto_path {%directory%}] == -1} then {
lappend ::auto_path {%directory%}
}
}]
}
} elseif {$language eq "tcl"} then {
if {[isEagle]} then {
tcl eval [tcl master] [string map [list %directory% $directory] {
if {![info exists ::auto_path] || \
[lsearch -exact $::auto_path {%directory%}] == -1} then {
lappend ::auto_path {%directory%}
}
}]
} else {
if {![info exists ::auto_path] || \
[lsearch -exact $::auto_path $directory] == -1} then {
lappend ::auto_path $directory
}
}
} else {
error "unsupported language, no idea how to modify auto-path"
}
}
#
# NOTE: This procedure adds a directory to the auto-path of the specified
# language (i.e. native Tcl or Eagle). The directory will not be
# added if it is already present. The language argument must be the
# literal string "eagle" or the literal string "tcl". The directory
# argument is the fully qualified path for the directory to add to
# the auto-path. The directory will not be added if it falls under
# a directory already in the auto-path.
#
proc maybeAddToAutoPath { language directory } {
#
# NOTE: Verify that the directory to be added is valid and exists. If
# not, do nothing.
#
if {[string length $directory] == 0 || \
![file isdirectory $directory]} then {
return false
}
#
# NOTE: Normalize the specified directory. This is necessary so that
# we can compare apples-to-apples within the auto-path.
#
set directory [file normalize $directory]
set directoryLength [string length $directory]
#
# NOTE: Query the auto-path for the target language.
#
set autoPath [getAutoPath $language]
#
# NOTE: Check each directory in the auto-path to see if the specified
# directory is already underneath it.
#
foreach autoDirectory $autoPath {
#
# NOTE: Normalize the auto-path directory. This is necessary so
# that we can compare apples-to-apples with the specified
# directory.
#
set autoDirectory [file normalize $autoDirectory]
set autoDirectoryLength [string length $autoDirectory]
#
# NOTE: Prefix match is impossible if the length of the specified
# directory is less than the length of this directory in the
# auto-path.
#
if {$directoryLength < $autoDirectoryLength} then {
continue
}
#
# NOTE: If the initial portion of the specified directory is the
# same as this directory in the auto-path, it must reside
# underneath it. In that case, there is no need to modify
# the auto-path, bail out now.
#
set last [expr {$autoDirectoryLength - 1}]
if {[string range $directory 0 $last] eq $autoDirectory} then {
return false
}
}
#
# NOTE: At this point, it is pretty safe to assume that the specified
# directory is not in the auto-path, nor underneath a directory
# within the auto-path.
#
addToAutoPath $language $directory
return true
}
#
# NOTE: This procedure verifies the combination of language and version
# specified by the caller. The language argument must be one of the
# literal strings "eagle", "tcl", or "client". 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 either "eagle" or "client". The varName argument is the name
# of a scalar variable in the context of the immediate caller that
# will receive a boolean value indicating if the specified language
# is actually a reference to the package downloader client itself.
#
proc verifyLanguageAndVersion { language version varName } {
if {[string length $varName] > 0} then {
upvar 1 $varName isClient
}
set isClient false
if {[string length $language] == 0 || $language eq "eagle"} then {
if {$version ne "1.0"} then {
error "unsupported Eagle version"
}
} elseif {$language eq "tcl"} then {
if {$version ne "8.4" && $version ne "8.5" && $version ne "8.6"} then {
error "unsupported Tcl version"
}
} elseif {$language eq "client"} then {
if {$version ne "1.0"} then {
error "unsupported client version"
}
set isClient true
} else {
error "unsupported language"
}
}
#
# NOTE: This procedure returns the name of the current platform. There are
# no arguments. An empty string will be returned if the name of the
# current platform cannot be determined for any reason.
#
proc getPlatform {} {
global tcl_platform
if {[info exists tcl_platform(platform)]} then {
set platform $tcl_platform(platform)
if {[info exists tcl_platform(machine)]} then {
set machine $tcl_platform(machine)
} else {
set machine ""
}
switch -exact -- $platform {
windows {
switch -exact -- $machine {
intel -
ia32_on_win64 {
return win32-x86
}
arm {
return win32-arm
}
ia64 {
return win64-ia64
}
amd64 {
return win64-x64
}
arm64 {
return win64-arm64
}
}
}
}
}
return ""
}
#
# NOTE: This procedure verifies the platform specified by the caller. 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". An empty string means that the
# associated entity does not require a specific platform. The
# varName argument is the name of a variable in the context of the
# immediate caller that will receive a modified platform name, if
# applicable. Upon failure, a script error will be raised. The
# return value is undefined.
#
proc verifyPlatform { platform varName } {
#
# NOTE: The platform name must be matched exactly and case-sensitively.
#
switch -exact -- $platform {
"" {
#
# NOTE: The empty string means "attempt to automatically detect" the
# necessary platform based on context information that may be
# present in the context of the immediate caller. If this is
# not possible, a script error will be raised.
#
upvar 1 language language
if {![info exists language]} then {
error "unable to detect language"
}
upvar 1 version version
if {![info exists version]} then {
error "unable to detect version"
}
upvar 1 packageName packageName
if {[info exists packageName]} then {
set localPackageName $packageName
} else {
set localPackageName ""
}
upvar 1 fileNames fileNames
if {[info exists fileNames]} then {
set localFileNames $fileNames
} else {
set localFileNames [list]
}
upvar 1 usePgp usePgp
if {[info exists usePgp]} then {
set localUsePgp $usePgp
} else {
set localUsePgp false
}
#
# NOTE: Download the list of platforms associated with this package
# from the package repository server. This may fail and raise
# a script error.
#
set platforms [downloadAllPlatforms \
$language $version $localPackageName $localFileNames $localUsePgp]
if {[string length $varName] > 0} then {
upvar 1 $varName newPlatform
}
#
# NOTE: Check the current platform and the neutral platform, in that
# order, to see if that platform is supported by the package
# being saught. If so, return that platform.
#
foreach thisPlatform [list [getPlatform] neutral] {
if {[lsearch -exact $platforms $thisPlatform] != -1} then {
set newPlatform $thisPlatform
return
}
}
#
# NOTE: If this point is reached, there are no supported platforms
# that are compatible with the current one for the specified
# package.
#
error "could not automatically detect platform"
}
neutral -
win32-arm -
win32-x86 -
win64-arm64 -
win64-ia64 -
win64-x64 {
#
# NOTE: This platform name is supported verbatim, do nothing.
#
}
default {
error "unsupported platform"
}
}
}
#
# NOTE: This procedure issues a request to an HTTP(S) server. It returns
# the raw response data verbatim. It may raise a script error. It
# will always use the currently configured HTTP(S) login cookie, if
# any; therefore, it should really only be used for requests to the
# package file server. The uri argument is the fully qualified URI
# to request.
#
proc getPackageFile { uri } {
variable loginCookie
variable quiet
if {[isEagle]} then {
if {![object invoke Eagle._Tests.Default \
TestHasScriptNewWebClientCallback ""]} then {
set error null
set code [object invoke Eagle._Tests.Default \
TestSetScriptNewWebClientCallback "" true true error]
if {$code ne "Ok"} then {
error [getStringFromObjectHandle $error]
}
}
if {[info exists loginCookie] && [llength $loginCookie] == 2} then {
set script [object create String {
if {[methodName ToString] eq "GetWebRequest"} then {
webRequest Headers.Add Cookie [join $loginCookie =]
}
}]
return [uri download -inline -webclientdata $script -- $uri]
} else {
return [uri download -inline -- $uri]
}
} else {
set options [list -binary true]
if {[info exists loginCookie] && [llength $loginCookie] == 2} then {
lappend options -headers [list Cookie [join $loginCookie =]]
}
return [eval ::PackageRepository::getFileViaHttp \
[list $uri] [list 20] [list stdout] [list $quiet] $options]
}
}
#
# NOTE: This procedure returns the prefix for fully qualified variable
# names that MAY be present in the global namespace. There are
# no arguments.
#
proc getDownloadVarNamePrefix {} {
return ::pkgd_; # TODO: Make non-global?
}
#
# NOTE: This procedure resets the currently configured login cookie, if
# any, and then attempts to login using the configured package
# repository server API key -OR- using the public access account.
# Upon success, it will set the login cookie to the one from the
# raw response data. Upon failure, a script error will be raised.
# There are no arguments.
#
# <public>
proc resetCookieAndLoginSimple {} {
variable publicPassword
variable publicUserName
set apiKey [lindex [::PackageRepository::getApiKeys \
[getDownloadVarNamePrefix] true] 0]
if {[string length $apiKey] > 0} then {
return [resetCookieAndLogin $apiKey $apiKey]
}
if {[string length $publicUserName] > 0 && \
[string length $publicPassword] > 0} then {
return [resetCookieAndLogin $publicUserName $publicPassword]
}
error "missing API keys and no public login credentials configured"
}
#
# NOTE: This procedure resets the currently configured login cookie, if
# any, and then attempts to login using the specified user name and
# password. Upon success, it will set the login cookie to the one
# from the raw response data. Upon failure, a script error will be
# raised. The userName argument must be the name of a package file
# server user with at least Fossil Check-Out (o) permissions on the
# package file server. The password argument must be the plaintext
# password that is associated with the specified user name.
#
# <public>
proc resetCookieAndLogin { userName password } {
variable baseUri
variable loginCookie
variable loginUri
variable loginUrn
#
# NOTE: Build the full URI for the login request.
#
set uri [subst $loginUri]
#
# NOTE: Reset the old login cookie, if any. Then, issue a new login
# request, capturing the raw response data.
#
set loginCookie [list]; set data [getPackageFile $uri]
#
# NOTE: Attempt to extract the necessary values from the raw response
# data.
#
set pattern(1) {"authToken":"(.*?)"}; # TODO: *HACK* Keep updated.
if {![regexp -- $pattern(1) $data dummy authToken]} then {
error "login response missing \"authToken\""
}
set pattern(2) {"loginCookieName":"(.*?)"}; # TODO: *HACK* Keep updated.
if {![regexp -- $pattern(2) $data dummy loginCookieName]} then {
error "login response missing \"loginCookieName\""
}
#
# NOTE: Set the login cookie to the one freshly extracted from the raw
# response data.
#
set loginCookie [list $loginCookieName $authToken]
#
# NOTE: Always return an empty string (i.e. and not any response data).
#
return ""
}
#
# NOTE: This procedure attempts to logout using the currently configured
# login cookie, if any, and then resets the login cookie. There
# are no arguments. This procedure may raise a script error.
#
# <public>
proc logoutAndResetCookie {} {
variable baseUri
variable loginCookie
variable logoutUri
variable logoutUrn
#
# NOTE: Attempt to verify that we are currently logged in.
#
if {![info exists loginCookie] || [llength $loginCookie] != 2} then {
error "missing or invalid login cookie"
}
#
# NOTE: Build the full URI for the logout request.
#
set authToken [lindex $loginCookie 1]
set uri [subst $logoutUri]
#
# NOTE: Reset the old login cookie, if any. Then, issue a new login
# request, capturing the raw response data.
#
set data [getPackageFile $uri]
#
# NOTE: Attempt to extract the necessary values from the raw response
# data.
#
set pattern(1) {"name":"nobody"}; # TODO: *HACK* Keep updated.
if {![regexp -- $pattern(1) $data dummy]} then {
error "logout response missing \"name\""
}
#
# NOTE: Reset the login cookie.
#
set loginCookie [list]
#
# NOTE: Always return an empty string (i.e. and not any response data).
#
return ""
}
#
# NOTE: This procedure checks if there is a higher version available of the
# specified package on the package file server. The language argument
# must be one of the literal strings "eagle", "tcl", or "client". 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 either "eagle" or "client". 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". An empty string means that the associated package
# does not require a specific platform. The packageName argument is
# a directory name relative to the language and version-specific
# directory on the package file server and may be an empty string.
# The usePgp argument should be non-zero when an OpenPGP signature
# file needs to be downloaded and verified for the downloaded file.
#
# <public>
proc checkForHigherVersion { language version platform packageName usePgp } {
variable clientDirectory
variable persistentRootDirectory
variable temporaryRootDirectory
verifyLanguageAndVersion $language $version isClient
verifyPlatform $platform platform
set temporaryDirectory [file join \
$temporaryRootDirectory [appendArgs \
pkgd_ver_ [::PackageRepository::getUniqueSuffix]]]
if {$isClient} then {
set persistentDirectory $clientDirectory
} else {
verifyPersistentRootDirectory
set persistentDirectory $persistentRootDirectory
}
set fileName [file join $packageName VERSION]
set downloadFileName [file join $temporaryDirectory $fileName]
file mkdir [file dirname $downloadFileName]
downloadOneFile $language $version $platform \
$fileName $downloadFileName $usePgp
if {$usePgp} then {
downloadOneFile $language $version $platform \
[appendArgs $fileName .asc] \
[appendArgs $downloadFileName .asc] $usePgp
}
set localFileName [file join $persistentDirectory $fileName]
set compare [package vcompare \
[string trim [readFile $downloadFileName]] \
[string trim [readFile $localFileName]]]
if {[isEagle]} then {
file delete -recursive -- $temporaryDirectory
} else {
file delete -force -- $temporaryDirectory
}
return [expr {$compare > 0}]
}
#
# NOTE: This procedure attempts to guess a package name based on a list of
# its files. It relies upon the fact that all packages must include
# a package index file. The language argument must be one of the
# literal strings "eagle", "tcl", or "client". The fileNames argument
# must be the list of file names to be downloaded. The package name,
# if one can be detected, is returned; otherwise, an empty string will
# be returned.
#
proc guessPackageNameFromFileNames { language fileNames } {
set packageIndexFileName [getPackageIndexFileName $language]
if {[string length $packageIndexFileName] > 0} then {
foreach fileName $fileNames {
set fileNameOnly [file tail $fileName]
if {$fileNameOnly eq $packageIndexFileName} then {
set directory [file dirname $fileName]
if {[string length $directory] > 0} then {
return [file tail $directory]
}
}
}
}
return ""
}
#
# NOTE: This procedure downloads a manitest from the package file server,
# writing its contents to the specified local file name. It can also
# verify the OpenPGP signature. The language argument must be one of
# the literal strings "eagle", "tcl", or "client". 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 either "eagle" or "client". The packageName argument
# is a directory name relative to the language and version-specific
# directory on the package file server and may be an empty string.
# The fileNames argument is the list of file names to be downloaded.
# The usePgp argument should be non-zero when an OpenPGP signature
# needs to be verified for the downloaded file.
#
proc downloadAllPlatforms { language version packageName fileNames usePgp } {
variable baseUri
variable platformsUri
variable platformsUrn
variable temporaryRootDirectory
set temporaryDirectory [file join \
$temporaryRootDirectory [appendArgs \
pkgd_plat_ [::PackageRepository::getUniqueSuffix]]]
set localFileName [file join $temporaryDirectory manifest.txt]
file mkdir [file dirname $localFileName]
#
# NOTE: First, build the final URI to download from the remote package
# repository.
#
set uri [subst $platformsUri]
#
# NOTE: Then, in one step, download the file from the package file
# server and write it to the specified local file.
#
downloadOneUriToFile $localFileName $uri $usePgp
#
# NOTE: Initialize list of platforms to return. This will be populated
# based on the platform directories available in the downloaded
# manfiest data.
#
set platforms [list]
#
# NOTE: Read the (OpenPGP verified) manifest data from the local file
# and split it into lines.
#
set data [readFile $localFileName]; set lines [split $data \n]
#
# NOTE: Figure out the pattern to use when matching against the file
# names in the manifest data. If available, this will include
# the package name; otherwise, platform names for all packages
# will be considered.
#
if {[string length $packageName] == 0} then {
set packageName [guessPackageNameFromFileNames $language $fileNames]
}
if {[string length $packageName] > 0} then {
set pattern [file join $language $version * $packageName *]
} else {
set pattern [file join $language $version *]
}
foreach line $lines {
if {[string range $line 0 1] eq "F "} then {
set fileName [lindex [split $line " "] 1]
if {[string match $pattern $fileName]} then {
set directory [lindex [file split $fileName] 2]
if {[string length $directory] > 0} then {
lappend platforms $directory
}
}
}
}
if {[isEagle]} then {
file delete -recursive -- $temporaryDirectory
} else {
file delete -force -- $temporaryDirectory
}
return [lsort -unique $platforms]
}
#
# NOTE: This procedure downloads a single URI from the package file server
# and writes the result to a local file. The localFileName argument
# is the file name where the downloaded file should be written. The
# The uri argument is the URI to download. The usePgp argument should
# be non-zero when the OpenPGP signature file needs to be verified for
# the downloaded file. The return value is undefined.
#
proc downloadOneUriToFile { localFileName uri usePgp } {
#
# NOTE: Then, in one step, download the URI from the package file
# server and write it to the specified local file.
#
writeFile $localFileName [getPackageFile $uri]
#
# NOTE: Is use of OpenPGP for signature verification enabled? Also,
# did we just download an OpenPGP signature file?
#
if {$usePgp && [isOpenPgpSignatureFileName $localFileName true]} then {
#
# NOTE: Attempt to verify the OpenPGP signature. If this fails,
# an error is raised.
#
::PackageRepository::openPgpMustBeInstalled
if {![::PackageRepository::verifyOpenPgpSignature $localFileName]} then {
error [appendArgs \
"bad OpenPGP signature \"" $localFileName \"]
}
}
}
#
# NOTE: This procedure downloads a single file from the package file server,
# writing its contents to the specified local file name. It can also
# verify the OpenPGP signatures. When an OpenPGP signature file is
# downloaded, this procedure assumes the corresponding data file was
# already downloaded (i.e. since OpenPGP needs both to perform the
# signature checks). The language argument must be one of the
# literal strings "eagle", "tcl", or "client". 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 either "eagle" or "client". 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". An empty
# string means that the associated package does not require a specific
# platform. The fileName argument is a file name relative to the
# language and version-specific directory on the package file server.
# The localFileName argument is the file name where the downloaded
# file should be written. The usePgp argument should be non-zero when
# the OpenPGP signature file needs to be verified for the downloaded
# file.
#
proc downloadOneFile {
language version platform fileName localFileName usePgp } {
variable baseUri
variable downloadUri
variable downloadUrn
#
# NOTE: First, build the full relative file name to download from
# the remote package repository.
#
set fileName [file join $language $version $platform $fileName]
set uri [subst $downloadUri]
#
# NOTE: Then, in one step, download the file from the package file
# server and write it to the specified local file.
#
downloadOneUriToFile $localFileName $uri $usePgp
}
#
# NOTE: This procedure attempts to download a list of files, optionally
# persistening them for subsequent uses by the target language.
# The language argument must be one of the literal strings "eagle",
# "tcl", or "client". 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 either "eagle"
# or "client". 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". An empty string means
# that the associated package does not require a specific platform.
# The fileNames argument must be a well-formed list of file names to
# download, each one relative to the language and version-specific
# directory on the package file server. The options argument must
# be a dictionary of name/value pairs. The -persistent option should
# be non-zero if the downloaded files should be saved to permanent
# storage for subsequent use. The -usePgp option should be non-zero
# when an OpenPGP signature file needs to be downloaded and verified
# for each downloaded file. The -useAutoPath option should be
# non-zero to modify the auto-path to include the temporary or
# persistent directories containing the downloaded files. The
# -allowUpdate option should be non-zero to allow existing package
# files to be overwritten.
#
# <public>
proc downloadFiles { language version platform fileNames options } {
variable clientDirectory
variable persistentRootDirectory
variable temporaryRootDirectory
variable viaInstall
verifyLanguageAndVersion $language $version isClient
verifyPlatform $platform platform
set temporaryDirectory [file join \
$temporaryRootDirectory [appendArgs \
pkgd_lib_ [::PackageRepository::getUniqueSuffix]]]
if {$isClient} then {
set persistentDirectory $clientDirectory
} else {
verifyPersistentRootDirectory
set persistentDirectory $persistentRootDirectory
}
set persistent [string is true -strict \
[getDictionaryValue $options -persistent]]
set usePgp [string is true -strict \
[getDictionaryValue $options -usePgp]]
set useAutoPath [string is true -strict \
[getDictionaryValue $options -useAutoPath]]
set allowUpdate [string is true -strict \
[getDictionaryValue $options -allowUpdate]]
set downloadedFileNames [list]
foreach fileName $fileNames {
if {[string length $fileName] == 0 || \
[file pathtype $fileName] ne "relative"} then {
error [appendArgs \
"bad file name \"" $fileName "\", not relative"]
}
set directoryParts [file split [file dirname $fileName]]
if {[llength $directoryParts] == 0} then {
error [appendArgs \
"bad file name \"" $fileName "\", no directory parts"]
}
set directory(temporary) [file normalize [eval \
file join [list $temporaryDirectory] $directoryParts]]
set directory(persistent) [file normalize [eval \
file join [list $persistentDirectory] $directoryParts]]
set fileNameOnly [file tail $fileName]
set downloadFileName [file normalize [file join \
$directory(temporary) $fileNameOnly]]
if {[file exists $downloadFileName]} then {
error [appendArgs \
"temporary file name \"" $downloadFileName \
"\" already exists"]
}
file mkdir [file dirname $downloadFileName]
downloadOneFile $language $version $platform \
$fileName $downloadFileName $usePgp
lappend downloadedFileNames [list \
$fileNameOnly $directory(temporary) $directory(persistent)]
if {$usePgp && \
![isOpenPgpSignatureFileName $downloadFileName true]} then {
downloadOneFile $language $version $platform \
[appendArgs $fileName .asc] \
[appendArgs $downloadFileName .asc] $usePgp
lappend downloadedFileNames [list \
[appendArgs $fileNameOnly .asc] $directory(temporary) \
$directory(persistent)]
}
}
set downloadDirectories [list]
foreach downloadedFileName $downloadedFileNames {
set directory(temporary) [lindex $downloadedFileName 1]
if {$persistent || $viaInstall} then {
set fileNameOnly [lindex $downloadedFileName 0]
set directory(persistent) [lindex $downloadedFileName 2]
file mkdir $directory(persistent)
set command [list file copy]
#
# NOTE: When updating the package repository client files, always
# use the -force option to overwrite existing files. Also,
# if we are allow updates, use the -force option.
#
if {$isClient || $allowUpdate} then {
lappend command -force
}
lappend command --
lappend command [file join $directory(temporary) $fileNameOnly]
lappend command [file join $directory(persistent) $fileNameOnly]
eval $command
lappend downloadDirectories $directory(persistent)
} else {
lappend downloadDirectories $directory(temporary)
}
}
#
# NOTE: Does the package need to be persisted locally? This can be set
# via the direct caller or via the installer tool.
#
set addPersistentDirectoryToAutoPath false
if {$persistent || $viaInstall} then {
#
# NOTE: In Eagle, a slightly different command is required to delete
# an entire directory tree.
#
if {[isEagle]} then {
file delete -recursive -- $temporaryDirectory
} else {
file delete -force -- $temporaryDirectory
}
#
# NOTE: When the target language is native Tcl, try to create the
# master package index, if necessary.
#
if {$language eq "tcl"} then {
maybeCreateMasterTclPackageIndex
set addPersistentDirectoryToAutoPath true
}
}
#
# NOTE: Sort the list of directories that downloaded files were written
# to, removing any duplicates in the process.
#
set downloadDirectories [lsort -unique $downloadDirectories]
if {$useAutoPath} then {
#
# NOTE: The auto-path, for whatever language this package belongs to,
# needs to be modified.
#
if {$addPersistentDirectoryToAutoPath} then {
#
# NOTE: The downloaded package was persisted -AND- will be handled
# by the master package index; therefore, just make sure the
# package persistence root directory is in the auto-path and
# then return only that directory in the result.
#
maybeAddToAutoPath $language $persistentDirectory
set downloadDirectories [list $persistentDirectory]
} else {
#
# NOTE: Check each unique download directory for a package index
# file. If a directory has a package index for the target
# language, add to the auto-path for the target language.
#
set packageIndexFileName [getPackageIndexFileName $language]
if {[string length $packageIndexFileName] > 0} then {
foreach downloadDirectory $downloadDirectories {
if {[file exists [file join \
$downloadDirectory $packageIndexFileName]]} then {
addToAutoPath $language $downloadDirectory
}
}
}
}
}
#
# NOTE: Always return the list of directories that were actually added
# to the auto-path, if any.
#
return $downloadDirectories
}
#
# 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: This package requires the package repository client package.
#
package require Eagle.Package.Repository
#
# 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.
#
setupDownloadVars [info script]
#
# NOTE: Setup the URI and URN variables, within this namespace, used by
# this script.
#
setupDownloadUriVars false; setupDownloadUrnVars false
#
# NOTE: If necessary, add the package persistence root directory to the
# auto-path for the current language. This will only be done if
# it falls outside of the existing auto-path.
#
variable persistentRootDirectory
maybeAddToAutoPath [expr {[isEagle] ? "eagle" : "tcl"}] \
$persistentRootDirectory
#
# NOTE: Provide the package to the interpreter.
#
package provide Eagle.Package.Downloader \
[expr {[isEagle] ? [info engine PatchLevel] : "1.0"}]
}