-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathtin.tcl
1516 lines (1412 loc) · 45.3 KB
/
tin.tcl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# tin.tcl
################################################################################
# Tcl/Git package installation manager and package development tools
# https://github.com/ambaker1/Tin
# Copyright (C) 2023 Alex Baker, ambaker1@mtu.edu
# All rights reserved.
# See the file "LICENSE" in the top level directory for information on usage,
# redistribution, and for a DISCLAIMER OF ALL WARRANTIES.
################################################################################
namespace eval ::tin {
# Internal variables
# Tin and Auto-Tin database dictionary variables
variable tinTin ""; # Installation info for packages and versions
# name {version {repo {tag file} ...} ...} ...
variable autoTin ""; # Auto-update information for packages
# name {repo {file reqs ...} ...} ...
variable autoFetch 1; # Boolean, whether to auto fetch.
# Define files for loading Tin: Tin comes prepackaged with a tinlist.tcl
# file, but the user can save their own added entries with "tin save".
variable myLocation [file dirname [file normalize [info script]]]
variable tinListFile [file join $myLocation tinlist.tcl]
variable userTinListFile [file normalize [file join ~ .tinlist.tcl]]
# Define the regular expression for getting version tags from GitHub.
# The pattern is compatible with the package version rules for Tcl, and
# additionally does not permit leading zeros, as per semver rules.
# Digit pattern for no leading zeros: (0|[1-9]\d*)
# https://semver.org/
# https://www.tcl.tk/man/tcl/TclCmd/package.html#M20
variable tagPattern \
{^v(0|[1-9]\d*)(\.(0|[1-9]\d*))*([ab](0|[1-9]\d*)(\.(0|[1-9]\d*))*)?$}
# Exported commands (ensemble with "tin")
## Modify the Tin and the Auto-Tin
namespace export add remove save clear reset
## Fetching and Auto-Fetching
namespace export fetch auto
## Query the Tin and the Auto-Tin
namespace export get packages versions available
## Tcl package utilities
namespace export installed forget
## Package install/uninstall commands
namespace export install depend uninstall
## Package upgrade commands
namespace export check upgrade
## Package loading commands
namespace export import require
## Package development utilities
namespace export mkdir bake assert
namespace ensemble create
}
## Modify the Tin and the Auto-Tin
################################################################################
# tin add --
#
# Add information for package and version for installation.
#
# Syntax
# tin add <-tin> $name $version $repo $tag $file
# tin add -auto $name $repo $file $args...
#
# Arguments:
# name Package name
# -tin Option to add Tin configuration info (default)
# -auto Option to add Auto-Tin configuration info
# version Package version
# repo Github repository URL
# tag Repository tag (not with -auto option)
# file Installer .tcl file (relative to repo main folder)
# requirement... Auto-Tin package version requirements (see PkgRequirements)
proc ::tin::add {args} {
variable tinTin
variable autoTin
switch [lindex $args 0] {
-tin { # Add to the Tin
set args [lrange $args 1 end]; # Strip -tin
if {[llength $args] != 5} {
WrongNumArgs "tin add ?-tin? name version repo tag file"
}
lassign $args name version repo tag file
ValidatePkgName $name
set version [NormalizeVersion $version]
dict set tinTin $name $version $repo [list $tag $file]
}
-auto { # Add to the Auto-Tin
set args [lrange $args 1 end]; # Strip -auto
if {[llength $args] < 3} {
WrongNumArgs "tin add -auto name repo file ?requirement...?"
}
set reqs [PkgRequirements {*}[lassign $args name repo file]]
ValidatePkgName $name
dict set autoTin $name $repo $file $reqs
}
default { # Default -tin
tin add -tin {*}$args
}
}
return
}
# tin remove --
#
# Remove entries from the Tin. Returns blank, does not complain.
# Essentially "dict unset" for Tin and Auto-Tin dictionaries.
#
# Syntax:
# tin remove $name ...; # Remove packages (Tin and Auto-Tin)
# tin remove -tin $name <$version> <$repo>; # Remove entries from Tin
# tin remove -auto $name <$repo> <$file>; # Remove entries from Auto-Tin
#
# Arguments:
# name Package name
# -tin Option to remove Tin entries
# -auto Option to remove Auto-Tin configurations
# version Package version in Tin
# repo Repository in Tin or Auto-Tin associated with package
# file Installer file in Auto-Tin for package and repo
proc ::tin::remove {args} {
variable tinTin
variable autoTin
switch [lindex $args 0] {
-tin { # Remove entries from the Auto-Tin
set args [lrange $args 1 end]; # Strip -tin
if {[llength $args] == 0 || [llength $args] > 3} {
WrongNumArgs "tin remove ?-tin? name ?version? ?repo?"
}
# Normalize version input
if {[llength $args] > 1} {
lset args 1 [NormalizeVersion [lindex $args 1]]
}
if {[dict exists $tinTin {*}$args]} {
dict unset tinTin {*}$args
}
}
-auto { # Remove entries from the Auto-Tin
set args [lrange $args 1 end]; # Strip -auto
if {[llength $args] == 0 || [llength $args] > 3} {
WrongNumArgs "tin remove -auto name ?repo? ?file?"
}
if {[dict exists $autoTin {*}$args]} {
dict unset autoTin {*}$args
}
}
default { # tin remove $name ...
foreach name $args {
tin remove -tin $name
tin remove -auto $name
}
}
}
return
}
# tin clear --
#
# Clears the Tin.
#
# Syntax:
# tin clear
proc ::tin::clear {} {
variable tinTin
variable autoTin
set tinTin ""
set autoTin ""
return
}
# tin save --
#
# Saves the Tin and Auto-Tin to the user tinlist file
#
# Syntax:
# tin save
proc ::tin::save {} {
variable tinTin
variable autoTin
variable tinListFile
variable userTinListFile
# Save current Tin and Auto-Tin, and reset to factory settings
set tin_save $tinTin
set auto_save $autoTin
tin reset -hard; # Resets to factory settings
# Open a temporary file for writing "tin add" commands to.
set fid [file tempfile tempfile]
# Write "tin add" commands for entries in the Tin
dict for {name data} $tin_save {
dict for {version data} $data {
dict for {repo data} $data {
if {![dict exists $tinTin $name $version $repo]} {
puts $fid [list tin add -tin $name $version $repo {*}$data]
}
}
}
}
# Write "tin add" commands for entries in the Auto-Tin
dict for {name data} $auto_save {
dict for {repo data} $data {
dict for {file reqs} $data {
if {![dict exist $autoTin $name $repo $file]} {
puts $fid [list tin add -auto $name $repo $file {*}$reqs]
}
}
}
}
# Write "tin remove" commands for entries in Tin.
dict for {name data} $tinTin {
if {![dict exists $tin_save $name]} {
puts $fid [list tin remove -tin $name]
continue
}
dict for {version data} $tinTin {
if {![dict exists $tin_save $name $version]} {
puts $fid [list tin remove -tin $name $version]
continue
}
dict for {repo data} $data {
if {![dict exists $tin_save $name $version $repo]} {
puts $fid [list tin remove -tin $name $version $repo]
}
}
}
}
# Write "tin remove" commands for entries in Auto-Tin.
dict for {name data} $autoTin {
if {![dict exists $auto_save $name]} {
puts $fid [list tin remove -auto $name]
continue
}
dict for {repo data} $tinTin {
if {![dict exists $auto_save $name $repo]} {
puts $fid [list tin remove -auto $name $repo]
continue
}
dict for {file reqs} $data {
if {![dict exists $auto_save $name $repo $file]} {
puts $fid [list tin remove -auto $name $repo $file]
}
}
}
}
# Copy the temp file over to the tin file.
close $fid
file copy -force $tempfile $userTinListFile
file delete -force $tempfile
return
}
# tin reset --
#
# Resets Tin and Auto-Tin to factory and user settings
#
# Syntax:
# tin reset <-soft | -hard>
#
# Arguments:
# -soft Option to reset to user settings (default)
# -hard Option to reset to factory settings
proc ::tin::reset {{option -soft}} {
variable tinListFile
variable userTinListFile
if {$option ni {-soft -hard}} {
return -code error "unknown option \"$option\": want -soft or -hard"
}
tin clear
source $tinListFile
if {$option eq "-soft" && [file exists $userTinListFile]} {
source $userTinListFile
}
return
}
## Fetching and Auto-Fetching
################################################################################
# tin auto --
#
# Toggle auto fetch on and off.
#
# Syntax:
# tin auto <$toggle>
#
# Arguments:
# toggle: Boolean, whether to automatically fetch to install/upgrade.
proc ::tin::auto {{toggle ""}} {
variable autoFetch
if {$toggle eq ""} {
return $autoFetch
}
if {![string is boolean -strict $toggle]} {
return -code error "toggle must be boolean"
}
return [set autoFetch $toggle]
}
# tin fetch --
#
# Update the Tin from GitHub repositories listed in the Auto-Tin.
# Regex pattern for tags defined at top of file.
# Returns list of versions fetched, or dictionary of package names and versions
#
# Syntax:
# tin fetch $name <$pattern>
# tin fetch -all <$names>
#
# Arguments:
# name Package name. Mutually exclusive with -all
# pattern Version number glob pattern. Default *
# -all Option to fetch all available versions.
# names List of packages to fetch for. Default all Auto-Tin packages.
proc ::tin::fetch {args} {
variable autoTin
variable tagPattern
# Handle "-all" case
if {[llength $args] == 0 || [lindex $args 0] eq "-all"} {
# tin fetch <-all>
# tin fetch -all <$names>
set args [lrange $args 1 end]
if {[llength $args] == 0} {
set names [tin packages -auto]
} elseif {[llength $args] == 1} {
set names [lindex $args 0]
} else {
WrongNumArgs "tin fetch -all ?names?"
}
# Fetch all versions of all packages (or list of packages)
set result ""
foreach name $names {
if {$name eq "-all"} {
return -code error "infinite loop"
}
set versions [tin fetch $name]
if {[llength $versions] > 0} {
dict set result $name $versions
}
}
# Return dictionary of package names and added versions
return $result
}
# tin fetch $name <$pattern>
set args [lassign $args name]
if {[llength $args] == 0} {
set pattern *
} elseif {[llength $args] == 1} {
set pattern [lindex $args 0]
} else {
WrongNumArgs "tin fetch name ?pattern?"
}
# Check if package is an Auto-Tin package (return blank)
if {![dict exists $autoTin $name]} {
return
}
# Loop through repositories for package
set versions ""
dict for {repo subdict} [tin get -auto $name] {
# Try to get version tags using git, and add valid ones to the Tin
try {
exec git ls-remote --tags $repo v$pattern
} on error {errMsg options} {
# Raise warning, but do not throw error.
puts "warning: failed to fetch tags for $name at $repo"
puts $errMsg
} on ok {result} {
# Acquired tag data. Strip excess data, and filter for regexp
set tags [lmap {~ path} $result {file tail $path}]
set tags [lsearch -inline -all -regexp $tags $tagPattern]
# Loop through tags, and add to the Tin if within specified reqs
foreach tag $tags {
set version [string range $tag 1 end]
dict for {file reqs} $subdict {
if {[package vsatisfies $version {*}$reqs]} {
tin add -tin $name $version $repo $tag $file
lappend versions $version
}
}
}; # end foreach tag
}; # end try
}; # end dict for
return $versions
}
## Query the Tin and the Auto-Tin
################################################################################
# tin get --
#
# Get raw information from the Tin or Auto-Tin. Returns blank if no info.
# Equivalent to "dict get" for Tin and Auto-Tin dictionaries, with the exception
# that it will not throw an error if the entry does not exist.
#
# Syntax:
# tin get <-tin> $name <$version> <$repo>
# tin get -auto $name <$repo> <$file>
#
# Arguments:
# name Package name (required)
# -tin Option to get Tin info (default)
# -auto Option to get Auto-Tin configuration info
# version Package version in Tin
# repo Repository in Tin or Auto-Tin associated with package
# file Installer file in Auto-Tin for package and repo
proc ::tin::get {args} {
variable tinTin
variable autoTin
switch [lindex $args 0] {
-tin {
# Get info from the Tin
set args [lrange $args 1 end]
if {[llength $args] < 1 || [llength $args] > 3} {
WrongNumArgs "tin get ?-tin? name ?version? ?repo?"
}
# Normalize version input
if {[llength $args] > 1} {
lset args 1 [NormalizeVersion [lindex $args 1]]
}
if {[dict exists $tinTin {*}$args]} {
return [dict get $tinTin {*}$args]
}
}
-auto {
# Get info from the Auto-Tin
set args [lrange $args 1 end]
if {[llength $args] < 1 || [llength $args] > 3} {
WrongNumArgs "tin get -auto name ?repo? ?file?"
}
if {[dict exists $autoTin {*}$args]} {
return [dict get $autoTin {*}$args]
}
}
default { # Default is -tin
return [tin get -tin {*}$args]
}
}
# Return blank otherwise
return
}
# tin packages --
#
# Get list of packages in the Tin or Auto-Tin, with optional "glob" pattern
#
# Syntax:
# tin packages <$pattern>
# tin packages -tin <$pattern>
# tin packages -auto <$pattern>
#
# Arguments:
# pattern Optional "glob" pattern for matching against package names
# -tin Option to only get packages from Tin
# -auto Option to only get packages from Auto-Tin
proc ::tin::packages {args} {
variable tinTin
variable autoTin
# tin packages
if {[llength $args] == 0} {
return [dict keys [dict merge $tinTin $autoTin]]
}
switch [lindex $args 0] {
-tin { # tin packages -tin <$pattern>
if {[llength $args] == 1} {
# tin packages -tin
return [dict keys $tinTin]
} elseif {[llength $args] == 2} {
# tin packages -tin $pattern
set pattern [lindex $args 1]
return [dict keys $tinTin $pattern]
} else {
WrongNumArgs "tin packages -tin ?pattern?"
}
}
-auto { # tin packages -auto <$pattern>
if {[llength $args] == 1} {
# tin packages -auto
return [dict keys $autoTin]
} elseif {[llength $args] == 2} {
# tin packages -auto $pattern
set pattern [lindex $args 1]
return [dict keys $autoTin $pattern]
} else {
WrongNumArgs "tin packages -auto ?pattern?"
}
}
default { # tin packages $pattern
if {[llength $args] == 1} {
set pattern [lindex $args 0]
return [dict keys [dict merge $tinTin $autoTin] $pattern]
} else {
WrongNumArgs "tin packages ?pattern?"
}
}
}
}
# tin versions --
#
# Get list of available versions for tin packages satisfying requirements
# List is unsorted
#
# Syntax:
# tin versions $name <$reqs...>
#
# Arguments:
# name Package name
# reqs... Package version requirements (see PkgRequirements)
proc ::tin::versions {name args} {
variable tinTin
if {![dict exists $tinTin $name]} {
return
}
# Get list of versions
set versions [dict keys [dict get $tinTin $name]]
# Filter for version requirements
if {[llength $args] > 0} {
set versions [FilterVersions $versions [PkgRequirements {*}$args]]
}
# Return unsorted list
return $versions
}
# tin available --
#
# Returns the version that would be installed with "tin installed".
# Calls fetch if no version is available.
# If not available, returns blank.
#
# Syntax:
# tin available $name <$reqs...>
#
# Arguments:
# name Package name
# reqs... Package version requirements (see PkgRequirements)
proc ::tin::available {name args} {
set reqs [PkgRequirements {*}$args]
if {![IsAvailable $name $reqs]} {
return
}
SelectVersion [tin versions $name] $reqs
}
## Package utilities
################################################################################
# tin installed --
#
# Returns the latest installed version meeting version requirements (normalized)
# Like "package present" but does not require package to be loaded.
# If not installed, returns blank
#
# Syntax:
# tin installed $name <$reqs...>
#
# Arguments:
# name Package name
# reqs... Package version requirements (see PkgRequirements)
proc ::tin::installed {name args} {
set reqs [PkgRequirements {*}$args]
if {![IsInstalled $name $reqs]} {
return
}
NormalizeVersion [SelectVersion [package versions $name] $reqs]
}
# tin forget --
#
# Package forget, but also deletes associated namespace.
#
# Syntax:
# tin forget $name ...
#
# Arguments:
# name Package name
proc ::tin::forget {args} {
foreach name $args {
package forget $name
if {[namespace exists ::$name]} {
namespace delete ::$name
}
}
return
}
## Package install/uninstall commands
################################################################################
# tin install --
#
# Install package from repository (does not check if already installed)
#
# Syntax:
# tin install $name <$reqs...>
#
# Arguments:
# name Package name
# reqs... Package version requirements (see PkgRequirements)
proc ::tin::install {name args} {
puts "searching in the Tin for $name $args ..."
set version [tin available $name {*}$args]
if {$version eq ""} {
return -code error "can't find $name $args in Tin"
}
# Now we know that there is a entry in the Tin for package "$name $version"
# The dict for loop will execute, and so will the try block.
# Loop through repositories for selected version
dict for {repo data} [tin get -tin $name $version] {
lassign $data tag file
try {
# Try to clone the repository into a temporary directory
close [file tempfile temp]
file delete $temp
file mkdir $temp
try {
exec git clone --depth 1 --branch $tag $repo $temp
} on error {result options} {
# Clean up and pass error if failed
if {[dict get $options -errorcode] ne "NONE"} {
file delete -force $temp
return -code error $result
}
}
# Install package from the cloned repository, in a fresh interpreter
puts "installing $name $version from $repo $tag ..."
set home [pwd]
cd $temp
set child [interp create]
try {
$child eval [list set tcl_library $::tcl_library]
$child eval [list source $file]
} on error {errMsg options} {
puts "error in running installer file"
return -options $options $errMsg
} finally {
# Clean up
interp delete $child
cd $home
file delete -force $temp
}
# Check for proper installation and return version
if {[IsInstalled $name $version-$version]} {
puts "$name version $version installed successfully"
} else {
return -code error "failed to install $name version $version"
}
} on error {errMsg options} {
# Catch the error, and try another repository (if any)
continue
}
# Try block was successful, return the version
return $version
}
# Re-raise the error in the "try" block (which will always execute)
return -options $options $errMsg
}
# tin depend --
#
# Requires that the package is installed. Returns installed version.
# Tries to install if package is missing, but does not load the package.
# Intended for package installer files.
#
# Syntax:
# tin depend $name <$reqs...>
#
# Arguments:
# name Package name
# reqs... Package version requirements (see PkgRequirements)
proc ::tin::depend {name args} {
# Try to install if the package is not installed
set version [tin installed $name {*}$args]
if {$version eq ""} {
puts "can't find package $name $args, attempting to install ..."
set version [tin install $name {*}$args]
}
return $version
}
# tin uninstall --
#
# Uninstalls versions of a package, as long as it is in the Tin or Auto-Tin
#
# Syntax:
# tin uninstall $name <$reqs...>
#
# Arguments:
# name Package name
# reqs... Package version requirements (see PkgRequirements)
proc ::tin::uninstall {name args} {
set reqs [PkgRequirements {*}$args]
# Check if package is available for install (cannot uninstall non-Tin pkgs)
if {![IsAvailable $name $reqs]} {
return -code error "cannot uninstall: $name $args not available"
}
# Check if package is installed (return if not) (updates index)
if {![IsInstalled $name $reqs]} {
return
}
# Loop through all installed versions meeting version requirements
foreach version [FilterVersions [package versions $name] $reqs] {
# Delete all "name-version" folders on the auto_path
set pkgFolder [PkgFolder $name $version]; # e.g. foo-1.0
foreach basedir $::auto_path {
set dir [file join [file normalize $basedir] $pkgFolder]
if {[file exists [file join $dir pkgUninstall.tcl]]} {
# Run pkgUninstall.tcl file to uninstall package.
# This allows for modifying files outside the package folder.
apply {{dir} {source [file join $dir pkgUninstall.tcl]}} $dir
} else {
# Just delete the package folder
file delete -force $dir
}
}
# Forget package
package forget $name $version
}
# Ensure package was uninstalled
if {[IsInstalled $name $reqs]} {
return -code error "failed to uninstall $name $args"
}
# Package was uninstalled. Return blank
return
}
## Package upgrading commands
################################################################################
# tin check --
#
# Check for upgradable packages
# Returns upgrade dictionary {name {old new ...} ...}, or list "old new"
#
# Syntax:
# tin check $name <$reqs...>
# tin check -all <$names>
proc ::tin::check {args} {
# tin check -all <$names>
if {[lindex $args 0] eq "-all"} {
if {[llength $args] > 2} {
WrongNumArgs "tin check -all ?names?"
}
# Create name-version result dictionary
set args [lrange $args 1 end]
if {[llength $args] == 1} {
set names [lindex $args 1]
} else {
set names [tin packages]
}
# Create upgrade result dictionary
set upgrades ""
foreach name $names {
if {$name eq "-all"} {
return -code error "infinite loop"
}
# Get latest installed package version
set version [tin installed $name]
if {$version eq ""} {
continue; # package is not installed
}
# Get maximum major version number
set n [lindex [SplitVersion $version] 0]
# Upgrade all major versions (if they exist)
for {set i 0} {$i <= $n} {incr i} {
set upgrade [tin check $name $i]; # "old new"
if {[llength $upgrade] > 0} {
dict set upgrades $name {*}$upgrade
}
}
}
return $upgrades
}
# tin check $name <$reqs ...>
set reqs [PkgRequirements {*}[lassign $args name]]
# Check if upgradable (return blank if not)
if {![IsUpgradable $name $reqs]} {
return
}
# Get old and new package versions
set old [SelectVersion [package versions $name] $reqs]
set new [SelectVersion [tin versions $name] $old $old]
return [list $old $new]
}
# tin upgrade --
#
# Upgrades packages (installs new, then uninstalls old)
# Returns the results of "tin check"
#
# Syntax:
# tin upgrade $name <$reqs...>
# tin upgrade -all <$names>
#
# Arguments:
# -all Option to upgrade all (major version 0-N, where N is largest)
# names Packages names. Default all Tin packages
# name Package name.
# reqs... Package version requirements (see PkgRequirements)
proc ::tin::upgrade {args} {
# tin upgrade -all <$names>
if {[lindex $args 0] eq "-all"} {
# tin upgrade -all $names
if {[llength $args] > 2} {
WrongNumArgs "tin check -all ?names?"
}
set upgrades [tin check {*}$args]
dict for {name oldnew} $upgrades {
dict for {old new} $oldnew {
puts "upgrading $name v$old to v$new ..."
tin install $name -exact $new
tin uninstall $name -exact $old
}
}
return $upgrades
}
# tin upgrade $name <$reqs ...>
set reqs [PkgRequirements {*}[lassign $args name]]
set upgrades [tin check $name {*}$reqs]
if {[llength $upgrades] == 2} {
lassign $upgrades old new
puts "upgrading $name v$old to v$new ..."
tin install $name -exact $new
tin uninstall $name -exact $old
}
return $upgrades
}
## Package loading commands, with installation on the fly
################################################################################
# tin import --
#
# Helper procedure to handle the majority of cases for importing Tcl packages
# Uses "tin require" to load the packages
# Returns versions
#
# Syntax
# tin import <-force> <$patterns from> $name <$reqs...> <as $ns>
#
# Arguments:
# -force Option to overwrite existing commands
# patterns Glob patterns for importing commands from package
# name Package name (must have corresponding namespace)
# reqs... User-input package version requirements (see PkgRequirements)
# ns Namespace to import into. Default global namespace.
#
# Examples
# tin import foo
# tin import -force foo
# tin import * from foo
# tin import -force bar from foo -exact 1.0 as f
proc ::tin::import {args} {
# <-force> <$patterns from> $name <$reqs...> <as $ns>
# Check for -force option
set force {}; # default
if {[lindex $args 0] eq "-force"} {
set args [lassign $args force]; # Strip from args
}
# <$patterns from> $name <$reqs...> <as $ns>
# Get patterns to import
set patterns {*}; # default
if {[lindex $args 1] eq "from"} {
set args [lassign $args patterns from]
}
# $name <$reqs...> <as $ns>
# Get namespace to import into
set ns {}; # default
if {[lindex $args end-1] eq "as"} {
set ns [lindex $args end]
set args [lrange $args 0 end-2]
}
# $name <$reqs...>
# Get package name
set args [lassign $args name]
# <$reqs...>
# Require package, import commands, and return version number
set version [uplevel 1 ::tin::require [linsert $args 0 $name]]
# Throw error if package does not have corresponding namespace.
if {![namespace exists ::$name]} {
return -code error "package $name does not have corresponding namespace"
}
# Add package name prefix to patterns, and import
set patterns [lmap pattern $patterns {string cat :: $name :: $pattern}]
namespace eval ::$ns [list namespace import {*}$force {*}$patterns]
return $version
}
# tin require --
#
# Same as "package require", but will try to install if needed.
#
# Syntax:
# tin require $name <$reqs...>
#
# Arguments:
# name Package name
# reqs... Package version requirements (see PkgRequirements)
proc ::tin::require {name args} {
# tin require $name $reqs ...
set reqs [PkgRequirements {*}$args]
# Return if package is present (this includes dynamically loaded packages)
if {![catch {package present $name {*}$reqs} version]} {
return $version
}
# Depend on package being installed, and call "package require"
tin depend $name {*}$args
tailcall ::package require $name {*}$reqs
}
## Package development utilities
################################################################################
# tin mkdir --
#
# Helper procedure to make library folders. Returns directory name.
# Intended for package installer files.
#
# Syntax:
# tin mkdir <-force> <$basedir> $name $version
#
# Arguments:
# -force Option to clear out the folder.
# basedir Optional, default one folder up from "info library"
# name Package name
# version Package version
proc ::tin::mkdir {args} {
# Extract the -force option from the input,
if {[lindex $args 0] eq "-force"} {
set force true
set args [lrange $args 1 end]
} else {
set force false
}
# Handle optional "basedir" input
if {[llength $args] == 3} {
lassign $args basedir name version
} elseif {[llength $args] == 2} {
set basedir [file dirname [info library]]
lassign $args name version
} else {
WrongNumArgs "tin mklib ?-force? ?basedir? name version"
}
# Create package directory, and return full path to user.
set dir [file join $basedir [PkgFolder $name $version]]
if {$force} {
file delete -force $dir
}
file mkdir $dir
return $dir
}
# tin bake --
#
# Perform substitution on file contents and write to new files
# Allows for uppercase alphanum variable substitution (e.g. @VARIABLE@)
# Intended for package build files.
#
# Syntax:
# tin bake $inFile $outFile <$config> <$varName $value...>
#
# Arguments:
# inFile File to read (with @VARIABLE@ declarations), or .tin folder
# outFile File to write to after substitution, or .tcl folder
# config Dictionary with keys of config variable names, and values.
# varName Configuration variable name (mutually exclusive with $config)
# value Configuration variable value (mutually exclusive with $config)
proc ::tin::bake {inFile outFile args} {
# Check arity
if {[llength $args] == 1} {
set config [lindex $args 0]
} elseif {[llength $args] % 2 == 0} {
set config $args
} else {