#!/usr/bin/tclsh

package require cmdline
package require textutil
package require sha256
package require json
package require doctools

## generate an image hash based on sorted package list
proc image_hash {packages} {
    return [::sha2::sha256 [lsort $packages]]
}

proc build_image {packages tag {path_prefix ""}} {
  puts "Building podman image (tag:$tag) with package list:"

  # Pretty print package list
  set packages_ln_separated [string map {" " \n} $packages]
  puts [textutil::indent [string map {" " \n} $packages_ln_separated] "   - "]

  # Build the image
    # BUG: I call the build command by unpacking arguments to ensure each package is passed as a separate argument. I'm not sure why
    # this is necessary. Probably a quoting error somewhere, maybe inside podman/dockerfile
  set build_cmd [list podman build --build-arg ADDITIONAL_PRODUCTS=$packages --label matlab-build-hash=[image_hash $packages] -t $tag ${path_prefix}.]
  exec {*}$build_cmd >@stdout 2>@stderr

  puts "Finished building image"
}

## Test if an image exists in the localhost repository
proc image_exists {image hash {checkhash 1}} {
  try {
    set image_info [json::json2dict [exec podman inspect $image]]
    set image_hash [dict get [lindex $image_info 0] Labels matlab-build-hash]

    if {(!$checkhash) || $image_hash eq $hash} {
      # If no hash specified or hash matches
      return IMAGE_EXISTS
    } else {
      return IMAGE_WRONG_HASH
    }
  } trap CHILDSTATUS {results options} {
    # Could not find image
    # I'm assuming the child errorcode is 125 (mixed results)
    return NOIMAGE
  } on error {results options} {
    error "Unknown error" $options [dict get $options -errorcode]
  }
}

## Find the latest image
proc latest_image {} {
  ## TODO: Should probably wrap this
  set latest_image_row [exec podman images --sort created --filter label=matlab-build-hash | sed {2q;d}]
  return [lindex $latest_image_row 0]:[lindex $latest_image_row 1]
}


set tag_default "localhost/matlab:custom-<hash>"

set options [ subst -nobackslashes -nocommands {
  {d.arg {} "Mount data directory"}
  {t.arg $tag_default "Tag for the podman image. Only has effect if the image needs to be rebuilt."}
  {package_file.arg {} "File containing packages to install"}
  {f                   "Force rebuild of the image"}
  {man                 "Open the man page"}
}]


set manSource {
[vset COMMAND_NAME "matlab_podman"]
[manpage_begin [vset COMMAND_NAME] 1 1.0]
[copyright {"Marek Hilton" 2026}]
[moddesc {}]
[keywords {podman matlab tcl tcllib}]
[titledesc {Build and run MATLAB as rootless podman containers}]
[description]
[list_begin definitions]
  [call [vset COMMAND_NAME] \[options\]]

  [def [option {-h --help}]] Show the help/usage information.

  [def [option {-t tag}]] Specify the tag of the image to be run/built.

  [def [option -f]] Force a rebuild of the image.

  [def [option "--packages_file [file <path>]"]] Read packages from the file at
    [file <path>] and ensure the image to be run/built contains these packages.
    If the image is specified with [option -t] and does not contain the specified
    packages, the command must be run with [option -f] to force a rebuild.

    The packages listed in [file <path>] should be newline separated.
  
  [def [option "-d [file <data_path>]"]] Mount [file <data_path>] folder
  under [file Documents/data] when running the container.

[list_end]

[section Introduction]

  [para]
  [cmd [vset COMMAND_NAME]] automates building and running MATLAB containers as
  rootless podman containers. This reduces the effort in using MATLAB on systems
  that have not packaged or do not support it.

  [para]
  The working directory is bind mounted to the Documents folder. Additionally,
  you can bind mount another folder (typically a data folder).

[section Troubleshooting]
  [list_begin definitions]
    [def "Process hangs"] Since rootless podman needs to chown the container to
    setup the user namespace, the initial run of this command may be slow and
    appear to hang once the image is built. Subsequent runs should be fast. On
    some filesystems, such as ZFS, since the images are large, the process may
    hang for a long/indefinite period. Consider using a different filesystem such
    as ext4 for podman storage.

    [def "Podman on root"] This script has not been tested with podman on root.
  [list_end]

  [section Dependencies]

  The following are dependencies for this script.
  [list_begin itemized]
    [item] tclsh
    [item] tcllib
    [item] podman
    [item] sed
    [item] chromium (optional for connecting to web interface)
    [item] man (optional for viewing man page)
    [item] groff (optional for viewing man page)
  [list_end]

[section Examples]

  Run the last image to be built.
  [example_begin]
      [vset COMMAND_NAME]
  [example_end]

  Run an image with the packages specified in [file packages.txt]. It will search
  for an image with a name [file matlab:custom-<hash>] where <hash> is unique to
  the set of packages specified. If not found, then a new image will be built.
  [example_begin]
      [vset COMMAND_NAME] --package-file packages.txt
  [example_end]

  Force a rebuild of the image.
  [example_begin]
      [vset COMMAND_NAME] --package-file packages.txt -f
  [example_end]


  Run the image with tag [file matlab:my-matlab]. If the image does not exist it
  will be created.
  [example_begin]
      [vset COMMAND_NAME] -t matlab:my-matlab
  [example_end]

  Run the image with tag [file matlab:my-matlab] and ensure it has the packages
  listed in [file packages.txt]. If the image does not exist/has the wrong
  packages, it will be built/rebuilt.
  [example_begin]
      [vset COMMAND_NAME] -t matlab:my-matlab -f
  [example_end]

[manpage_end]
}


try {
  array set params [::cmdline::getoptions argv $options {}]
} trap {CMDLINE USAGE} {msg o} {
  doctools::new dt -format text

  ## Using sed to strip the rest of the manpage. Lazy but works.
  exec sed -n {/^Introduction/q;/DESCRIPTION/,$p} << [dt format $manSource] >@stdout 2>@stderr
  dt destroy
  exit 0
}

## Open man page if asked for, then quit.
if {$params(man)} {
  doctools::new dt -format nroff
  set roff [dt format $manSource]
  dt destroy

  set terminal_width [expr [exec tput cols] - 5]
  set terminal_width [expr min($terminal_width,72)]
  # TODO: Use the PAGER env variable
  exec groff -Tutf8 -rLL=${terminal_width}n -rLT=${terminal_width}n -man - | less -R << $roff >@stdout 2>@stderr
  exit 0
}


## Environment
puts "Script folder:     [set script_path [file dirname [file normalize [info script]]]/]"
puts "Working folder:    [set working_dir $::env(PWD)/]"
puts "UID directory:     [set uid [exec id -u]]"
puts "GID:               [set gid [exec id -g]]"

###########
## Setup ##
###########

# If packages file is specified then read it
if {!($params(package_file) eq "")} {
  set f [open $params(package_file) r]
  try {
    set packages [read $f]
  } finally {
    close $f
  }
  set packages [lsort $packages]
}

## Calculate package hash to find/build image. If packages are specified then
## set flag `check_hash` to indicate the image hash should be verified
if {[info exists packages]} {
  set check_hash 1
} else {
  set check_hash 0
  set packages ""
}
set hash [image_hash $packages]

puts "Package hash: $hash"

## 1. If no tag was given then use default tag and substitute hash into the tag
## 2. If neither packages nor tag specified, choose latest matlab image to launch
## 3. If tag specified then leave unchanged
if {($params(t) eq $tag_default) && $check_hash} {
  set tag [string map "<hash> [string range $hash 0 8]" $tag_default]
} elseif {($params(t) eq $tag_default) && !$check_hash} {
  # TODO: handle no latest image found
  set tag [latest_image]
} else {
  set tag $params(t)
}

puts "Tag: $tag"

################
## Build Step ##
################
# + Image does not exist -> build
# + Image exists -> skip
# + Image exists and has mismatch -> exit
# + Image exists, force flag, and has mismatch -> delete & build

switch [image_exists $tag $hash $check_hash] {
  NOIMAGE {
    puts "No image with tag $tag"
    build_image $packages $tag $script_path
  }
  IMAGE_WRONG_HASH {
    puts "Found image with tag $tag but wrong package list hash"
    if {$params(f)} {
      puts "WARNING: deleting $tag"
      exec podman image rm $tag
      build_image $packages $tag $script_path
    } else {
      puts "Image exists with wrong packages. -f to force rebuild"
      exit 17
    }
  }
  IMAGE_EXISTS {
    puts "Found image with tag $tag."
  }
}

##############
## Run step ##
##############


set podman_args "\
  run\
  --rm -it\
  --userns=keep-id:uid=1001,gid=1001\
  --shm-size=512M\
  -v $working_dir:/home/matlab/Documents\
  [if {$params(d) eq ""} {} {list -v $params(d):/home/matlab/Documents/data}]\
  -p 8888:8888\
  $tag\
  -browser" 

set podman_cmd "podman $podman_args"

set podman [open |$podman_cmd r]
fconfigure $podman -blocking 0 -buffering line
set matlab_reached 0

fileevent $podman readable {
  if {[gets $podman line] >= 0} {
    if {!$matlab_reached} {
      after 0 {exec chromium --app=http://127.0.0.1:8888}
      set matlab_reached 1
    }
    puts "MATLAB: $line"
    flush stdout
  } elseif {[eof $podman]} {
    close $podman
  }
}



vwait forever 
