1#!/usr/bin/tclsh
2#
3# This script is used to quickly test a VSIX (Visual Studio Extension) file
4# with Visual Studio 2015 on Windows.
5#
6# PREREQUISITES
7#
8# 1. This tool is Windows only.
9#
10# 2. This tool must be executed with "elevated administrator" privileges.
11#
12# 3. Tcl 8.4 and later are supported, earlier versions have not been tested.
13#
14# 4. The "sqlite-UWP-output.vsix" file is assumed to exist in the parent
15#    directory of the directory containing this script.  The [optional] first
16#    command line argument to this script may be used to specify an alternate
17#    file.  However, currently, the file must be compatible with both Visual
18#    Studio 2015 and the Universal Windows Platform.
19#
20# 5. The "VERSION" file is assumed to exist in the parent directory of the
21#    directory containing this script.  It must contain a version number that
22#    matches the VSIX file being tested.
23#
24# 6. The temporary directory specified in the TEMP or TMP environment variables
25#    must refer to an existing directory writable by the current user.
26#
27# 7. The VS140COMNTOOLS environment variable must refer to the Visual Studio
28#    2015 common tools directory.
29#
30# USAGE
31#
32# The first argument to this script is optional.  If specified, it must be the
33# name of the VSIX file to test.
34#
35package require Tcl 8.4
36
37proc fail { {error ""} {usage false} } {
38  if {[string length $error] > 0} then {
39    puts stdout $error
40    if {!$usage} then {exit 1}
41  }
42
43  puts stdout "usage:\
44[file tail [info nameofexecutable]]\
45[file tail [info script]] \[vsixFile\]"
46
47  exit 1
48}
49
50proc isWindows {} {
51  #
52  # NOTE: Returns non-zero only when running on Windows.
53  #
54  return [expr {[info exists ::tcl_platform(platform)] && \
55      $::tcl_platform(platform) eq "windows"}]
56}
57
58proc isAdministrator {} {
59  #
60  # NOTE: Returns non-zero only when running as "elevated administrator".
61  #
62  if {[isWindows]} then {
63    if {[catch {exec -- whoami /groups} groups] == 0} then {
64      set groups [string map [list \r\n \n] $groups]
65
66      foreach group [split $groups \n] {
67        #
68        # NOTE: Match this group line against the "well-known" SID for
69        #       the "Administrators" group on Windows.
70        #
71        if {[regexp -- {\sS-1-5-32-544\s} $group]} then {
72          #
73          # NOTE: Match this group line against the attributes column
74          #       sub-value that should be present when running with
75          #       elevated administrator credentials.
76          #
77          if {[regexp -- {\sEnabled group(?:,|\s)} $group]} then {
78            return true
79          }
80        }
81      }
82    }
83  }
84
85  return false
86}
87
88proc getEnvironmentVariable { name } {
89  #
90  # NOTE: Returns the value of the specified environment variable or an empty
91  #       string for environment variables that do not exist in the current
92  #       process environment.
93  #
94  return [expr {[info exists ::env($name)] ? $::env($name) : ""}]
95}
96
97proc getTemporaryPath {} {
98  #
99  # NOTE: Returns the normalized path to the first temporary directory found
100  #       in the typical set of environment variables used for that purpose
101  #       or an empty string to signal a failure to locate such a directory.
102  #
103  set names [list]
104
105  foreach name [list TEMP TMP] {
106    lappend names [string toupper $name] [string tolower $name] \
107        [string totitle $name]
108  }
109
110  foreach name $names {
111    set value [getEnvironmentVariable $name]
112
113    if {[string length $value] > 0} then {
114      return [file normalize $value]
115    }
116  }
117
118  return ""
119}
120
121proc appendArgs { args } {
122  #
123  # NOTE: Returns all passed arguments joined together as a single string
124  #       with no intervening spaces between arguments.
125  #
126  eval append result $args
127}
128
129proc readFile { fileName } {
130  #
131  # NOTE: Reads and returns the entire contents of the specified file, which
132  #       may contain binary data.
133  #
134  set file_id [open $fileName RDONLY]
135  fconfigure $file_id -encoding binary -translation binary
136  set result [read $file_id]
137  close $file_id
138  return $result
139}
140
141proc writeFile { fileName data } {
142  #
143  # NOTE: Writes the entire contents of the specified file, which may contain
144  #       binary data.
145  #
146  set file_id [open $fileName {WRONLY CREAT TRUNC}]
147  fconfigure $file_id -encoding binary -translation binary
148  puts -nonewline $file_id $data
149  close $file_id
150  return ""
151}
152
153proc putsAndEval { command } {
154  #
155  # NOTE: Outputs a command to the standard output channel and then evaluates
156  #       it in the callers context.
157  #
158  catch {
159    puts stdout [appendArgs "Running: " [lrange $command 1 end] ...\n]
160  }
161
162  return [uplevel 1 $command]
163}
164
165proc isBadDirectory { directory } {
166  #
167  # NOTE: Returns non-zero if the directory is empty, does not exist, -OR- is
168  #       not a directory.
169  #
170  catch {
171    puts stdout [appendArgs "Checking directory \"" $directory \"...\n]
172  }
173
174  return [expr {[string length $directory] == 0 || \
175      ![file exists $directory] || ![file isdirectory $directory]}]
176}
177
178proc isBadFile { fileName } {
179  #
180  # NOTE: Returns non-zero if the file name is empty, does not exist, -OR- is
181  #       not a regular file.
182  #
183  catch {
184    puts stdout [appendArgs "Checking file \"" $fileName \"...\n]
185  }
186
187  return [expr {[string length $fileName] == 0 || \
188      ![file exists $fileName] || ![file isfile $fileName]}]
189}
190
191#
192# NOTE: This is the entry point for this script.
193#
194set script [file normalize [info script]]
195
196if {[string length $script] == 0} then {
197  fail "script file currently being evaluated is unknown" true
198}
199
200if {![isWindows]} then {
201  fail "this tool only works properly on Windows"
202}
203
204if {![isAdministrator]} then {
205  fail "this tool must run with \"elevated administrator\" privileges"
206}
207
208set path [file normalize [file dirname $script]]
209set argc [llength $argv]; if {$argc > 1} then {fail "" true}
210
211if {$argc == 1} then {
212  set vsixFileName [lindex $argv 0]
213} else {
214  set vsixFileName [file join \
215      [file dirname $path] sqlite-UWP-output.vsix]
216}
217
218###############################################################################
219
220if {[isBadFile $vsixFileName]} then {
221  fail [appendArgs \
222      "VSIX file \"" $vsixFileName "\" does not exist"]
223}
224
225set versionFileName [file join [file dirname $path] VERSION]
226
227if {[isBadFile $versionFileName]} then {
228  fail [appendArgs \
229      "Version file \"" $versionFileName "\" does not exist"]
230}
231
232set projectTemplateFileName [file join $path vsixtest.vcxproj.data]
233
234if {[isBadFile $projectTemplateFileName]} then {
235  fail [appendArgs \
236      "Project template file \"" $projectTemplateFileName \
237      "\" does not exist"]
238}
239
240set envVarName VS140COMNTOOLS
241set vsDirectory [getEnvironmentVariable $envVarName]
242
243if {[isBadDirectory $vsDirectory]} then {
244  fail [appendArgs \
245      "Visual Studio 2015 directory \"" $vsDirectory \
246      "\" from environment variable \"" $envVarName \
247      "\" does not exist"]
248}
249
250set vsixInstaller [file join \
251    [file dirname $vsDirectory] IDE VSIXInstaller.exe]
252
253if {[isBadFile $vsixInstaller]} then {
254  fail [appendArgs \
255      "Visual Studio 2015 VSIX installer \"" $vsixInstaller \
256      "\" does not exist"]
257}
258
259set envVarName ProgramFiles
260set programFiles [getEnvironmentVariable $envVarName]
261
262if {[isBadDirectory $programFiles]} then {
263  fail [appendArgs \
264      "Program Files directory \"" $programFiles \
265      "\" from environment variable \"" $envVarName \
266      "\" does not exist"]
267}
268
269set msBuild [file join $programFiles MSBuild 14.0 Bin MSBuild.exe]
270
271if {[isBadFile $msBuild]} then {
272  fail [appendArgs \
273      "MSBuild v14.0 executable file \"" $msBuild \
274      "\" does not exist"]
275}
276
277set temporaryDirectory [getTemporaryPath]
278
279if {[isBadDirectory $temporaryDirectory]} then {
280  fail [appendArgs \
281      "Temporary directory \"" $temporaryDirectory \
282      "\" does not exist"]
283}
284
285###############################################################################
286
287set installLogFileName [appendArgs \
288    [file rootname [file tail $vsixFileName]] \
289    -install- [pid] .log]
290
291set commands(1) [list exec [file nativename $vsixInstaller]]
292
293lappend commands(1) /quiet /norepair
294lappend commands(1) [appendArgs /logFile: $installLogFileName]
295lappend commands(1) [file nativename $vsixFileName]
296
297###############################################################################
298
299set buildLogFileName [appendArgs \
300    [file rootname [file tail $vsixFileName]] \
301    -build-%configuration%-%platform%- [pid] .log]
302
303set commands(2) [list exec [file nativename $msBuild]]
304
305lappend commands(2) [file nativename [file join $path vsixtest.sln]]
306lappend commands(2) /target:Rebuild
307lappend commands(2) /property:Configuration=%configuration%
308lappend commands(2) /property:Platform=%platform%
309
310lappend commands(2) [appendArgs \
311    /logger:FileLogger,Microsoft.Build.Engine\;Logfile= \
312    [file nativename [file join $temporaryDirectory \
313    $buildLogFileName]] \;Verbosity=diagnostic]
314
315###############################################################################
316
317set uninstallLogFileName [appendArgs \
318    [file rootname [file tail $vsixFileName]] \
319    -uninstall- [pid] .log]
320
321set commands(3) [list exec [file nativename $vsixInstaller]]
322
323lappend commands(3) /quiet /norepair
324lappend commands(3) [appendArgs /logFile: $uninstallLogFileName]
325lappend commands(3) [appendArgs /uninstall:SQLite.UWP.2015]
326
327###############################################################################
328
329if {1} then {
330  catch {
331    puts stdout [appendArgs \
332        "Install log: \"" [file nativename [file join \
333        $temporaryDirectory $installLogFileName]] \"\n]
334  }
335
336  catch {
337    puts stdout [appendArgs \
338        "Build logs: \"" [file nativename [file join \
339        $temporaryDirectory $buildLogFileName]] \"\n]
340  }
341
342  catch {
343    puts stdout [appendArgs \
344        "Uninstall log: \"" [file nativename [file join \
345        $temporaryDirectory $uninstallLogFileName]] \"\n]
346  }
347}
348
349###############################################################################
350
351if {1} then {
352  putsAndEval $commands(1)
353
354  set versionNumber [string trim [readFile $versionFileName]]
355  set data [readFile $projectTemplateFileName]
356  set data [string map [list %versionNumber% $versionNumber] $data]
357
358  set projectFileName [file join $path vsixtest.vcxproj]
359  writeFile $projectFileName $data
360
361  set platforms [list x86 x64 ARM]
362  set configurations [list Debug Release]
363
364  foreach platform $platforms {
365    foreach configuration $configurations {
366      putsAndEval [string map [list \
367          %platform% $platform %configuration% $configuration] \
368          $commands(2)]
369    }
370  }
371
372  putsAndEval $commands(3)
373}
374