jul.tcl 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. #!/usr/bin/tclsh
  2. # Copyright 2015,2016,2017 Lucas Sköldqvist <frusen@dragora.org>
  3. # License: GPLv3
  4. package require sqlite3
  5. set version "0.5.4"
  6. set arch ""
  7. array set repolist {
  8. gungre {
  9. {frusen kelsoo mprodrigues tom mmpg}
  10. gungre.db
  11. http://gungre.ch/jul/
  12. }
  13. }
  14. if {[file exists $::env(HOME)/.julrc] == 1} {
  15. source $::env(HOME)/.julrc
  16. } elseif {[file exists $::env(HOME)/.jul/config.tcl] == 1} {
  17. source $::env(HOME)/.jul/config.tcl
  18. }
  19. proc list_repo {args} {
  20. global repolist
  21. global arch
  22. # if there is no argument, search for all
  23. if {[lindex $args 0] == {}} {
  24. set query "WHERE"
  25. } else {
  26. set query "WHERE name LIKE '%$args%' and"
  27. }
  28. if {$arch != ""} {
  29. if {$query != ""} {
  30. append query " and arch = '$arch' and"
  31. } else {
  32. set query "WHERE arch = '$arch' and"
  33. }
  34. }
  35. set result {}
  36. foreach repo [lreverse [array names repolist]] {
  37. set db_file $::env(HOME)/.jul/[get_db_file $repo]
  38. if {[catch {sqlite3 db $db_file -create false} fid]} {
  39. puts stderr "jul: search: Unable to open database file."
  40. return
  41. }
  42. foreach re [lindex $repolist($repo) 0] {
  43. db eval "SELECT rowid,* FROM package $query repo='$re'" {
  44. set pkg(rowid) $rowid
  45. set pkg(name) $name
  46. set pkg(version) $version
  47. set pkg(repo) $repo
  48. set pkg(arch) $arch
  49. set pkg(build) $build
  50. db eval "SELECT desc FROM description \
  51. JOIN package USING(name) \
  52. WHERE lang = 'en' AND name='$name'" {
  53. set pkg(desc) $desc
  54. }
  55. lappend result [array get pkg]
  56. }
  57. }
  58. db close
  59. }
  60. return $result
  61. }
  62. # Returns the file name of the repository 'name'.
  63. proc get_db_file {name} {
  64. global repolist
  65. return [lindex $repolist($name) 1]
  66. }
  67. proc add {args} {
  68. global repolist
  69. set cmd [lindex $args 0]
  70. set z 0
  71. set c 0
  72. # receive a list of all the packages
  73. set pkg_list [list_repo [lindex $args 1]]
  74. if {[llength $pkg_list] == 0} {
  75. puts "jul: $cmd: No packages found."
  76. return
  77. }
  78. set i 1
  79. set finds {}
  80. foreach key $pkg_list {
  81. array set pkg $key
  82. lappend finds $pkg(rowid)
  83. puts -nonewline " \[$i\] $pkg(repo) $pkg(name)-$pkg(version)"
  84. puts "-$pkg(arch)-$pkg(build)"
  85. array unset pkg
  86. incr i
  87. }
  88. set z 0
  89. set c 0
  90. while {$z == 0} {
  91. puts "Select a package to $cmd or `q' to quit."
  92. puts -nonewline "Pressing `enter' selects the top package: "
  93. flush stdout
  94. set c [gets stdin]
  95. set z [checkanswer $c [llength $pkg_list]]
  96. # If enter was pressed, select the first package.
  97. if {$z == 2} {set c "1"}
  98. }
  99. # lists start at 0
  100. incr c -1
  101. set result {}
  102. foreach repo [lreverse [array names repolist]] {
  103. set db_file $::env(HOME)/.jul/[get_db_file $repo]
  104. if {[catch {sqlite3 db $db_file -create false} fid]} {
  105. puts stderr "jul: $cmd: Unable to open database file."
  106. return
  107. }
  108. db eval "SELECT * FROM repository JOIN package \
  109. ON repository.name = package.repo \
  110. WHERE package.rowid=[lindex $finds $c]" {
  111. set p(name) $name
  112. set p(version) $version
  113. set p(arch) $arch
  114. set p(build) $build
  115. set p(url) $url
  116. lappend result [array get p]
  117. }
  118. db close
  119. }
  120. foreach item [getplist] {
  121. if {"$p(name)-$p(version)-$p(arch)-$p(build)" == $item} {
  122. puts "jul: $cmd: Package already installed."
  123. return
  124. }
  125. }
  126. cd "$::env(HOME)/.jul/cache"
  127. set fn $p(name)-$p(version)-$p(arch)-$p(build).tlz
  128. set pkg $p(url)$p(name)/$fn
  129. puts -nonewline "Downloading $p(name)... "
  130. flush stdout
  131. getfile $pkg
  132. puts -nonewline "Downloading checksum... "
  133. getfile $pkg.sha1sum
  134. puts -nonewline "Verifying... "
  135. if {[verify_file $fn.sha1sum] == -1} {
  136. return -1
  137. }
  138. puts "done"
  139. set pfile $::env(HOME)/.jul/cache/$fn
  140. catch {exec su -c "pkg $cmd $pfile"} results options
  141. puts $results
  142. }
  143. # List the changes in 'repo'.
  144. proc changes {repo} {
  145. global repolist
  146. if {$repo == ""} {
  147. puts "jul: changes: You must specify a repository."
  148. exit
  149. } elseif {$repo == "-h"} {
  150. help changes
  151. }
  152. if {[lsearch -exact [lreverse [array names repolist]] $repo] == -1} {
  153. puts "jul: changes: `$repo' is not a valid repository."
  154. exit
  155. }
  156. if {[file exists $::env(HOME)/.jul/repos/$repo.changes] == 1} {
  157. set f [open $::env(HOME)/.jul/repos/$repo.changes]
  158. fcopy $f stdout
  159. close $f
  160. } else {
  161. puts -nonewline "jul: changes: Can't list changes for `$repo'."
  162. puts " Try synchronising."
  163. }
  164. }
  165. # Check if 'value' is in 'range' and return 1 if that is the case. Exit if
  166. # 'value' is 'q'.
  167. proc checkanswer {value range} {
  168. if {$value == "q"} {exit}
  169. if {$value == ""} {return 2}
  170. if {[string is integer -strict $value] == 1} {
  171. # $value < $range because we start to count from 0 and
  172. # array size does not
  173. if {[expr {$value >= 1}] && [expr {$value <= $range}]} {
  174. return 1
  175. }
  176. }
  177. return 0
  178. }
  179. # Remove '$HOME/.jul'.
  180. proc clean {args} {
  181. if {$args == "-h"} {help clean}
  182. if {$args != "-y"} {
  183. puts "You're about to delete `$::env(HOME)/.jul/' and all of its "
  184. puts "content."
  185. puts -nonewline "Proceed? (Y/n) "
  186. flush stdout
  187. set c [read stdin 1]
  188. if {$c == "n" || $c == "N"} {
  189. puts "Aborted"
  190. } else {
  191. file delete -force $::env(HOME)/.jul
  192. puts "Deleted"
  193. }
  194. } else {
  195. file delete -force $::env(HOME)/.jul
  196. puts "Deleted $::env(HOME)/.jul and all of its content."
  197. }
  198. }
  199. proc getfile {url} {
  200. if {[catch {exec curl -sfO $url} results options]} {
  201. set details [dict get $options -errorcode]
  202. puts "failed"
  203. puts -nonewline "Could not download file: "
  204. if {[lindex $details 0] eq "CHILDSTATUS"} {
  205. set status [lindex $details 2]
  206. if {$status == 22} {
  207. puts "HTTP error code > 400"
  208. puts "The file was probably not found."
  209. puts "Please report this!"
  210. } elseif {$status == 23} {
  211. puts "Write error in $::env(PWD)"
  212. }
  213. } elseif {[lindex $details 1] eq "ENOENT"} {
  214. puts "Could not find `curl'. Make sure it is installed."
  215. } else {
  216. puts "Unknown error. Please report this!"
  217. }
  218. } else {
  219. puts "done"
  220. }
  221. }
  222. proc verify_file {fn} {
  223. if {[catch {exec sha1sum -c $fn} results options]} {
  224. puts "failed\n"
  225. puts $results
  226. puts ""
  227. return -1
  228. }
  229. }
  230. # Print the usage of the command passed as 'args' or the help screen if no
  231. # command is passed.
  232. proc help {args} {
  233. if {$args == ""} {
  234. usage
  235. exit
  236. }
  237. switch -exact $args {
  238. changes {
  239. puts "Usage: jul changes \[options\] repository"
  240. puts "Shows the changelog of repository.\n"
  241. puts "Changes command options:"
  242. puts " -h display this help and exit"
  243. }
  244. clean {
  245. puts "Usage: jul clean \[options\]"
  246. puts "Removes ~/.jul and all of its content.\n"
  247. puts "Clean command options:"
  248. puts " -h display this help and exit"
  249. puts " -y skip (y/N) prompt"
  250. }
  251. default {
  252. puts "jul: help: $args no such command."
  253. }
  254. }
  255. exit
  256. }
  257. # Lists the installed packages.
  258. proc listpkgs {pattern} {
  259. foreach item [getplist] {
  260. if {$pattern != ""} {
  261. if {[string match "*$pattern*" $item] == 1} {
  262. puts $item;
  263. }
  264. } else {
  265. puts $item
  266. }
  267. }
  268. }
  269. # Return a sorted list of all installed packages.
  270. proc getplist {} {
  271. set lst {}
  272. foreach file [glob -nocomplain -directory \
  273. "/var/db/pkg" -tails -types f *] {
  274. lappend lst $file
  275. }
  276. return [lsort $lst]
  277. }
  278. proc printColumnarLines {lines} {
  279. foreach fields $lines {
  280. set col 0
  281. foreach field $fields {
  282. set w [string length $field]
  283. if {![info exist width($col)] || $width($col) < $w} {
  284. set width($col) $w
  285. }
  286. incr col
  287. }
  288. }
  289. foreach fields $lines {
  290. set col 0
  291. foreach field $fields {
  292. puts -nonewline [format "%-*s " $width($col) $field]
  293. incr col
  294. }
  295. puts "";
  296. }
  297. }
  298. # Search for packages.
  299. proc search {args} {
  300. # receive a list of all the packages
  301. set pkg_list [list_repo [lindex $args 0]]
  302. # if the number of elements in $pkg_list is 0, put an error and return
  303. if {[llength $pkg_list] == 0} {
  304. puts "jul: search: No packages found."
  305. return
  306. }
  307. set lines {}
  308. foreach key $pkg_list {
  309. array set p $key
  310. lappend lines [list $p(repo) \
  311. $p(name)-$p(version)-$p(arch)-$p(build) $p(desc)]
  312. }
  313. printColumnarLines $lines
  314. }
  315. proc lstrepo {} {
  316. # fill finds with all available packages
  317. array set finds [list_repo [lindex "" 0]]
  318. for {set x 0} {$x < [array size finds]} {incr x} {
  319. set s [split_pkg [lindex $finds($x) 0]]
  320. puts $s
  321. }
  322. }
  323. # Split the package string 'p' into a list and return it.
  324. proc split_pkg {p} {
  325. # remove the trailing '.tlz'
  326. set p [string trimright $p ".tlz"]
  327. # split 'p' at every '-' found
  328. set psplit [split $p -]
  329. if {[llength $psplit] > 4} {
  330. # number of dashes in the package name part
  331. set dashes [expr {[llength $psplit] - 4}]
  332. # the new package name, with dashes
  333. set newname [join [lrange $psplit 0 $dashes] -]
  334. # replace the elements for the 'package name' with 'newname'
  335. set psplit [lreplace $psplit 0 $dashes $newname]
  336. }
  337. return $psplit
  338. }
  339. proc update {} {
  340. lstrepo
  341. }
  342. # Get and verify files.
  343. proc dosync {repo} {
  344. global repolist
  345. puts "$repo syncing"
  346. puts -nonewline "Downloading [get_db_file $repo]... "
  347. getfile [lindex $repolist($repo) 2][get_db_file $repo]
  348. puts -nonewline "Downloading checksum... "
  349. getfile [lindex $repolist($repo) 2][get_db_file $repo].sha1sum
  350. puts -nonewline "Verifying... "
  351. if {[verify_file $repo.db.sha1sum] == -1} {
  352. return -1
  353. }
  354. puts "done"
  355. }
  356. # TODO: refactor
  357. proc sync {} {
  358. global repolist
  359. if {[file exists $::env(HOME)/.jul/cache] == 0} {
  360. file mkdir $::env(HOME)/.jul/cache
  361. }
  362. cd "$::env(HOME)/.jul"
  363. # loop through all repositories
  364. foreach repo [lreverse [array names repolist]] {
  365. set rpo [get_db_file $repo]
  366. # synchronise if there is no database
  367. if {[file isfile $rpo] == 0} {
  368. dosync $repo
  369. } else {
  370. # read the local and remote version
  371. # TODO: only reads first line
  372. set f [open $::env(HOME)/.jul/$rpo.sha1sum]
  373. set lver [gets $f]
  374. close $f
  375. set rver_file [lindex $repolist($repo) 2]$rpo.sha1sum
  376. if {[catch {set rver [exec curl -sf $rver_file]} results options]} {
  377. set details [dict get $options -errorcode]
  378. puts "Trying to download $rver_file"
  379. puts -nonewline "Could not download file: "
  380. if {[lindex $details 0] eq "CHILDSTATUS"} {
  381. set status [lindex $details 2]
  382. if {$status == 22} {
  383. puts "HTTP error code > 400"
  384. puts "The file was probably not found."
  385. puts "Please report this!"
  386. } elseif {$status == 23} {
  387. puts "Write error in $::env(PWD)"
  388. }
  389. } else {
  390. puts "Unknown error. Please report this!"
  391. }
  392. exit
  393. }
  394. # if the local and remote versions are the same, verify
  395. # the files
  396. if {$lver == $rver} {
  397. puts "$repo is up to date"
  398. puts -nonewline "Verifying... "
  399. # synchronise if the verification fails
  400. if {[verify_file $repo.db.sha1sum] == -1} {
  401. dosync $repo
  402. } else {
  403. puts "done"
  404. }
  405. } else {
  406. dosync $repo
  407. }
  408. }
  409. }
  410. }
  411. proc usage {} {
  412. puts "Usage: jul <command> \[options] \[package|keyword|command]"
  413. puts "\nCommands:"
  414. puts " changes lists recent changes in the repositories"
  415. puts " clean removes ~/.jul and all of its content"
  416. puts " help display information for a command or this screen"
  417. puts " add/install fetch and install packages from repositories"
  418. puts " list list installed or downloaded packages"
  419. puts " search search repositories for packages"
  420. puts " sync synchronise with repositories"
  421. puts " upgrade fetch and upgrade packages from repositories"
  422. puts " version show version of this program"
  423. }
  424. if {$argc > 0} {
  425. switch -exact [lindex $::argv 0] {
  426. add {add add [lindex $::argv 1]}
  427. changes {changes [lindex $::argv 1]}
  428. clean {clean [lindex $::argv 1]}
  429. help {help [lindex $::argv 1]}
  430. install {add add [lindex $::argv 1]}
  431. "list" {listpkgs [lindex $::argv 1]}
  432. search {search [lindex $::argv 1]}
  433. sync {sync}
  434. update {update}
  435. upgrade {add upgrade [lindex $::argv 1]}
  436. version {puts "This is jul version $version"}
  437. default {puts "jul: [lindex $::argv 0]: No such command."}
  438. }
  439. } else {
  440. usage
  441. }