###############################################################################
#
# csharp.eagle --
#
# Extensible Adaptable Generalized Logic Engine (Eagle)
# Eagle CSharp Package File
#
# 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 ::Eagle {
  #
  # NOTE: This procedure is used to determine the fully qualified path to the
  #       .NET Core SDK.  An empty string will be returned to indicate an
  #       error.  This procedure should not raise script errors.
  #
  proc getDotNetCoreSdkPath {} {
    if {[catch {exec -- dotnet --info} info] == 0} then {
      set info [string map [list \r\n \n] $info]

      if {[regexp -line -- \
          {^\s*Base Path:\s+([^\n]+)$} $info dummy path]} then {
        return [file normalize $path]
      }
    }

    return ""
  }

  #
  # NOTE: This procedure is used to determine the fully qualified path to the
  #       directory containing the reference assemblies for the .NET Standard
  #       2.0.  An empty string will be returned to indicate an error.  This
  #       procedure should not raise script errors.
  #
  proc getDotNetStandardReferencePath {
          {packageVersion ""} {standardVersion netstandard2.0} } {
    set path [getDotNetCoreSdkPath]

    if {[string length $path] > 0} then {
      set libraryDirectory [file normalize [file join \
          [file dirname $path] NuGetFallbackFolder netstandard.library]]

      set buildReferenceSubDirectory [file join build $standardVersion ref]

      if {[string length $packageVersion] > 0} then {
        set assemblyDirectory [file normalize [file join \
            $libraryDirectory $packageVersion $buildReferenceSubDirectory]]

        if {[file exists $assemblyDirectory]} then {
          return $assemblyDirectory
        }
      } else {
        set globPathPattern [file join $libraryDirectory *]

        set maybeVersions [lsort -decreasing -command [list package vsort] \
            [lmap directory [glob -nocomplain -types {d} $globPathPattern] \
            { file tail $directory }]]

        foreach maybeVersion $maybeVersions {
          set assemblyDirectory [file normalize [file join \
              $libraryDirectory $maybeVersion $buildReferenceSubDirectory]]

          if {[file exists $assemblyDirectory]} then {
            return $assemblyDirectory
          }
        }
      }
    }

    return ""
  }

  #
  # NOTE: This procedure is used to obtain a test program for use with the
  #       C# compiler.  Upon success, the return value will be a list with
  #       two elements.  The first element will be the name of the C# class
  #       to be compiled.  The second element will be the C# program text.
  #       Upon failure, the return value will be an empty list.
  #
  proc getCSharpTestProgram { {name ""} } {
    set prefix Test
    set id [object invoke Interpreter.GetActive NextId]

    set className [appendArgs \
        $prefix Namespace $id [object invoke Type Delimiter] \
        $prefix Class $id]

    return [list $className [subst {
      using System;

      namespace ${prefix}Namespace${id}
      {
        public static class ${prefix}Class${id}
        {
          public static Int32 Main(String\[\] args)
          {
            return 0;
          }
        }
      }
    }]]
  }

  #
  # NOTE: This procedure is used to determine whether the C# compiler appears
  #       to work when invoked via the interfaces defined in this script file.
  #       Non-zero is returned to indicate success.  This procedure should not
  #       raise script errors.
  #
  proc doesCompileCSharpWork { {name ""} {errorsVarName ""} } {
    if {[string length $errorsVarName] > 0} then {
      upvar 1 $errorsVarName errors
    }

    if {[catch {getCSharpTestProgram $name} program]} then {
      set errors [list [appendArgs \
          "caught error while getting \"" $name "\" test program: " \
          $program]]

      return false
    }

    if {[llength $program] < 2} then {
      set errors [list [appendArgs \
          "command \[getCSharpTestProgram\] returned malformed \"" \
          $name "\" test program: " $program]]

      return false
    }

    unset -nocomplain results local_errors

    if {[catch {
      compileCSharp [lindex $program 1] true true true results local_errors
    } code]} then {
      set errors [list [appendArgs \
          "caught error while compiling \"" $name "\" test program: " \
          $code]]

      return false
    }

    if {$code ne "Ok"} then {
      set errors [list [appendArgs \
          "errors from compilation of \"" $name "\" test program: " \
          $local_errors]]

      return false
    }

    if {[catch {
      object invoke [lindex $program 0] Main null
    } exitCode]} then {
      set errors [list [appendArgs \
          "caught error while executing \"" $name "\" test program: " \
          $exitCode]]

      return false
    }

    if {$exitCode ne "0"} then {
      set errors [list [appendArgs \
          "bad exit code from \"" $name "\" test program: " \
          $exitCode]]

      return false
    }

    return true
  }

  #
  # NOTE: This procedure is used to determine the command line arguments that
  #       are required to invoke the .NET Core SDK compiler for C#.  An empty
  #       list will be returned if the arguments cannot be determined for some
  #       reason -OR- the C# compiler cannot be found.  This procedure should
  #       not raise script errors.
  #
  proc getDotNetCoreCSharpCommandArgs {} {
    set path [getDotNetCoreSdkPath]

    if {[string length $path] > 0} then {
      set compilerFileName [file normalize [file join \
          $path Roslyn bincore csc.dll]]

      if {[file exists $compilerFileName]} then {
        return [list dotnet exec [appendArgs \
            \" [file nativename $compilerFileName] \"]]
      }
    }

    return [list]
  }

  #
  # NOTE: This procedure is used to format an option to the C# compiler.  It
  #       may have a name and/or a value.  This procedure should not raise
  #       script errors.
  #
  proc formatCompilerArgument { name value } {
    set wrap ""

    if {[regexp -- {\s} $name] || [regexp -- {\s} $value]} then {
      set wrap \"
    }

    if {[string length $name] > 0} then {
      if {[string length $value] > 0} then {
        return [appendArgs $wrap $name : $value $wrap]
      } else {
        return [appendArgs $wrap $name $wrap]
      }
    } else {
      if {[string length $value] > 0} then {
        return [appendArgs $wrap $value $wrap]
      } else {
        return ""
      }
    }
  }

  #
  # NOTE: This procedure is used to translate a name/value pair into zero or
  #       more options to the C# compiler.  This procedure should not raise
  #       script errors.
  #
  proc compilerParameterToArguments {
          name {value ""} {outputAssemblyVarName ""} } {
    switch -exact -nocase -- $name {
      WarningLevel {
        set name -warn
      }
      TreatWarningsAsErrors {
        set name -warnaserror
      }
      OutputAssembly {
        if {[string length $outputAssemblyVarName] > 0} then {
          #
          # HACK: This compiler parameter is handled by our caller; however,
          #       we want to honor the value specified via the OutputAssembly
          #       property.  Therefore, reset the specified variable from the
          #       caller to the new value.
          #
          upvar 1 $outputAssemblyVarName outputAssembly

          #
          # NOTE: Use the file name value specified by the caller verbatim.
          #
          set outputAssembly $value

          #
          # HACK: Also, make sure that we do not handle this parameter again,
          #       below.
          #
          set name ""; set value ""
        } else {
          #
          # BUGBUG: Translate the compiler parameter; however, this may not
          #         actually work, depending on how our caller handles its
          #         output assembly file name.  At the time this block was
          #         originally written (2018-04-09), the only caller (i.e.
          #         [compileViaDotNetCoreCSharp]) always passed the output
          #         assembly variable name, making this a non-issue.  This
          #         convention should also be followed by future callers of
          #         this procedure.
          #
          set name -out
        }
      }
      ReferencedAssemblies.Add {
        set name -reference

        if {[file pathtype $value] ne "absolute"} then {
          set value [file nativename [file normalize \
              [file join [getDotNetStandardReferencePath] \
              $value]]]
        }
      }
    }

    set formatted [formatCompilerArgument $name $value]

    if {[string length $formatted] > 0} then {
      return [list $formatted]
    } else {
      return [list]
    }
  }

  #
  # NOTE: This procedure is used to obtain the base command line options for
  #       the C# compiler, including those that may be enabled by default.
  #       An empty string may be returned.  This procedure should not raise
  #       script errors.
  #
  proc getCSharpCompilerOptions { parameters library csharp prefix } {
    #
    # NOTE: Make sure that the "standard" preprocessor defines match those
    #       for the platform (i.e. the ones used to compile the Eagle core
    #       library assembly).  This caller may disable this handling.
    #
    if {$library} then {
      set libraryOptions [expr { \
          [info exists ::eagle_platform(compileOptions)] ? \
          $::eagle_platform(compileOptions) : [list] \
      }]
    } else {
      set libraryOptions [list]
    }

    #
    # NOTE: Permit extra C# compiler options to be passed via the global
    #       array element "csharpOptions", if it exists.  This caller may
    #       disable this handling.
    #
    if {$csharp} then {
      set csharpOptions [expr { \
          [info exists ::eagle_platform(csharpOptions)] ? \
          $::eagle_platform(csharpOptions) : [list] \
      }]
    } else {
      set csharpOptions [list]
    }

    #
    # NOTE: Start out with no compiler options.
    #
    set result ""

    #
    # NOTE: Grab the existing compiler options, if any.  This caller may
    #       disable this handling (e.g. by specifying an invalid opaque
    #       object handle for the "parameters" argument).
    #
    if {[isNonNullObjectHandle $parameters]} then {
      if {[string length $result] > 0} then {
        append result " "
      }

      append result [$parameters CompilerOptions]
    }

    #
    # NOTE: Are there any Eagle core library options to check?
    #
    if {[llength $libraryOptions] > 0} then {
      #
      # NOTE: Was the Eagle core library built in the Debug configuration?
      #
      if {"DEBUG" in $libraryOptions} then {
        if {[string length $result] > 0} then {
          append result " "
        }

        append result $prefix define:DEBUG
      }

      #
      # NOTE: Was the Eagle core library built with tracing enabled (i.e.
      #       this allows for use of System.Diagnostics.Trace, etc)?
      #
      if {"TRACE" in $libraryOptions} then {
        if {[string length $result] > 0} then {
          append result " "
        }

        append result $prefix define:TRACE
      }
    }

    #
    # NOTE: Are there any extra C# compiler options to add?
    #
    if {[llength $csharpOptions] > 0} then {
      #
      # NOTE: Append the configured extra C# compiler options configured
      #       via the global array element "csharpOptions", if any.
      #
      foreach csharpOption $csharpOptions {
        if {[string length $result] > 0} then {
          append result " "
        }

        append result $csharpOption
      }
    }

    return $result
  }

  #
  # NOTE: This procedure is used to escape all characters in the specified
  #       string for use inside of a regular expression.  An empty string
  #       may be returned.  This procedure should not raise script errors.
  #
  proc regexpEscapeAll { value } {
    set result ""

    foreach char [split $value ""] {
      append result \\u [format %04X [string ordinal $char 0]]
    }

    return $result
  }

  #
  # NOTE: This procedure is used to execute the C# compiler and returns its
  #       platform normalized results.
  #
  proc runDotNetCSharpCommand { command } {
    #
    # NOTE: Evaluate the [exec] command constructed by our caller, in their
    #       context, and return the results, with line-endings normalized.
    #
    return [string map [list \r\n \n] [uplevel 1 $command]]
  }

  #
  # NOTE: This procedure is used to extract the C# compiler error messages
  #       from its results.  An empty list will be returned if the errors
  #       cannot be determined for some reason.  This procedure should not
  #       raise script errors.
  #
  proc extractCSharpErrors { fileName results } {
    set list [list]

    foreach {dummy match} [regexp -all -line -inline -- \
        [appendArgs (^(?: [regexpEscapeAll $fileName] \
        {\(\d+,\d+\): )?error CS\d{4}: [^\n]+$)}] $results] {
      lappend list $match
    }

    return $list
  }

  #
  # NOTE: This procedure is used to extract the C# compiler warning messages
  #       from its results.  An empty list will be returned if the warnings
  #       cannot be determined for some reason.  This procedure should not
  #       raise script errors.
  #
  proc extractCSharpWarnings { fileName results } {
    set list [list]

    foreach {dummy match} [regexp -all -line -inline -- \
        [appendArgs (^(?: [regexpEscapeAll $fileName] \
        {\(\d+,\d+\): )?warning CS\d{4}: [^\n]+$)}] $results] {
      lappend list $match
    }

    return $list
  }

  #
  # NOTE: This procedure is used to dynamically compile arbitrary C# code
  #       from within a script using the CSharpCodeProvider class present
  #       in the desktop .NET Framework.  It may work on some versions of
  #       Mono as well.  This procedure was originally designed to be used
  #       by the test suite; however, it can be quite useful in non-test
  #       scripts as well.
  #
  proc compileViaCSharpCodeProvider {
          string memory symbols strict resultsVarName errorsVarName args } {
    #
    # NOTE: The [object] command is required by this procedure.  If it
    #       is not available, bail out now.
    #
    if {[llength [info commands object]] == 0} then {
      #
      # NOTE: We cannot even attempt to compile anything, fail.
      #
      set code Error

      #
      # NOTE: Prepare to transfer error messages to the caller.
      #
      if {[string length $errorsVarName] > 0} then {
        upvar 1 $errorsVarName local_errors
      }

      #
      # NOTE: Append to the list of errors.
      #
      lappend local_errors "cannot compile, missing \"object\" command"

      #
      # NOTE: Return the overall result to the caller.
      #
      return $code
    }

    #
    # NOTE: Create the C# code provider object (i.e. the compiler).
    #
    set provider [object create -alias Microsoft.CSharp.CSharpCodeProvider]

    #
    # NOTE: Create the object that provides various parameters to the C#
    #       code provider (i.e. the compiler options).
    #
    set parameters [object create -alias \
        System.CodeDom.Compiler.CompilerParameters]

    #
    # NOTE: Do we not want to persist the generated assembly to disk?
    #
    set outputFileName ""

    if {$memory} then {
      $parameters GenerateInMemory true
    } else {
      $parameters OutputAssembly [set outputFileName [appendArgs [set \
          tempName(1) [file tempname]] .dll]]
    }

    #
    # NOTE: Use a try/finally block to cleanup temporary files.
    #
    try {
      #
      # NOTE: Do we want symbols to be generated for the generated assembly?
      #
      if {$symbols} then {
        $parameters IncludeDebugInformation true
      }

      #
      # NOTE: Start out the compiler options with the pre-existing defaults
      #       for the compiler followed by those necessary for the platform.
      #
      $parameters CompilerOptions \
          [getCSharpCompilerOptions $parameters true true /]

      #
      # NOTE: Process extra compiler settings the caller may have provided.
      #
      foreach {name value} $args {
        $parameters -nocase $name $value
      }

      #
      # NOTE: Prepare to transfer the object reference to the caller.  We
      #       must use [upvar] here because otherwise the object is lost
      #       when the procedure call frame is cleaned up.
      #
      if {[string length $resultsVarName] > 0} then {
        upvar 1 $resultsVarName results
      }

      #
      # NOTE: Attempt to compile the specified string as C# and capture the
      #       results into the variable provided by the caller.
      #
      set results [$provider -alias CompileAssemblyFromSource $parameters \
          $string]

      #
      # NOTE: We no longer need the C# code provider object (i.e. the
      #       compiler); therefore, dispose it now.
      #
      unset provider; # dispose

      #
      # NOTE: Fetch the collection of compiler errors (which may be empty).
      #
      set errors [$results -alias Errors]

      #
      # NOTE: It is assumed that no assembly was generated if there were
      #       any compiler errors.  Ignore all compiler warnings unless
      #       we are in strict mode.
      #
      if {[$errors HasErrors] || \
          ($strict && [$errors HasWarnings])} then {
        #
        # NOTE: Compilation of the assembly failed.
        #
        set code Error

        #
        # NOTE: Prepare to transfer error messages to the caller.
        #
        if {[string length $errorsVarName] > 0} then {
          upvar 1 $errorsVarName local_errors
        }

        #
        # NOTE: Grab each error object and append the string itself to
        #       the overall list of errors.
        #
        for {set index 0} {$index < [$errors Count]} {incr index} {
          #
          # NOTE: Get the compiler error object at this index.
          #
          set error [$errors -alias Item $index]

          #
          # NOTE: Convert it to a string and append it to the list of
          #       errors.
          #
          lappend local_errors [$error ToString]

          #
          # NOTE: Since the error itself is actually an object, we must
          #       dispose it.
          #
          unset error; # dispose
        }
      } else {
        #
        # NOTE: Compilation of the assembly succeeded.
        #
        set code Ok
      }

      #
      # NOTE: We no longer need the compiler errors collection; therefore,
      #       dispose it now.
      #
      unset errors; # dispose

      #
      # HACK: *BREAKING CHANGE* If there is an output file name, return it
      #       as well; otherwise, just return success.
      #
      if {[string length $outputFileName] > 0} then {
        #
        # NOTE: Return a two element list: the first element is the overall
        #       result and the second element is the output file name.
        #
        return [list $code $outputFileName]
      } else {
        #
        # NOTE: Return the overall result to the caller.
        #
        return $code
      }
    } finally {
      #
      # NOTE: Make sure the dummy temporary files are cleaned up.
      #
      if {[array exists tempName]} then {
        foreach tempFileName [array values tempName] {
          if {[file exists $tempFileName]} then {
            catch {file delete $tempFileName}
          }
        }
      }
    }
  }

  #
  # NOTE: This procedure is used to dynamically compile arbitrary C# code
  #       from within a script using the command line C# compiler provided
  #       by the .NET Core SDK.  This procedure was originally designed to
  #       be used by the test suite; however, it can be quite useful in
  #       non-test scripts as well.
  #
  proc compileViaDotNetCoreCSharp {
          string memory symbols strict resultsVarName errorsVarName args } {
    #
    # NOTE: Get the initial command line arguments needed to invoke the C#
    #       compiler on .NET Core.  If this ends up being invalid, nothing
    #       else can be done.
    #
    set command [getDotNetCoreCSharpCommandArgs]

    if {[llength $command] == 0} then {
      #
      # NOTE: We cannot even attempt to compile anything, fail.
      #
      set code Error

      #
      # NOTE: Prepare to transfer error messages to the caller.
      #
      if {[string length $errorsVarName] > 0} then {
        upvar 1 $errorsVarName local_errors
      }

      #
      # NOTE: Append to the list of errors.
      #
      lappend local_errors "cannot compile, C# compiler was not found"

      #
      # NOTE: Return the overall result to the caller.
      #
      return $code
    }

    #
    # NOTE: Insert the [exec] command before the command line arguments.
    #       The -success option is not used here because we want to handle
    #       errors (only) by processing the compiler output.
    #
    set command [linsert $command 0 exec --]

    #
    # NOTE: Start out the compiler options with the pre-existing defaults
    #       for the compiler followed by those necessary for the platform.
    #
    append command " " [getCSharpCompilerOptions "" true true -]

    #
    # NOTE: Allocate a couple temporary file names, one to hold the source
    #       code to compile and one to hold the generated assembly.
    #
    set sourceFileName [appendArgs [set tempName(1) [file tempname]] .cs]
    set outputFileName [appendArgs [set tempName(2) [file tempname]] .dll]

    #
    # NOTE: Use a try/finally block to cleanup temporary files.
    #
    try {
      #
      # NOTE: Process extra compiler settings the caller may have provided.
      #
      foreach {name value} $args {
        set nameValueArguments \
            [compilerParameterToArguments $name $value outputFileName]

        if {[llength $nameValueArguments] > 0} then {
          eval lappend command $nameValueArguments
        }
      }

      #
      # NOTE: Make the compiler output a little quieter.  This is needed
      #       to maintain compatibility with the results generated by the
      #       [compileViaCSharpCodeProvider] procedure.
      #
      lappend command -nologo

      #
      # NOTE: Always build as a library so that we do not require a static
      #       Main method.
      #
      lappend command -target:library

      #
      # NOTE: If symbols are enabled, add the necessary command line
      #       argument.
      #
      if {$symbols} then {lappend command -debug}

      #
      # NOTE: As of this writing (2018-04-06), the current version of the
      #       .NET Core SDK (2.1.101) uses the "netstandard.dll" assembly
      #       to enable use of the .NET Standard 2.0 library.
      #
      lappend command [appendArgs \"-reference: [file nativename \
          [file normalize [file join [getDotNetStandardReferencePath] \
          netstandard.dll]]] \"]

      #
      # NOTE: Set the output assembly file name to the temporary output
      #       file name we obtained from [file tempname] above.
      #
      lappend command [appendArgs \"-out: [file nativename [file \
          normalize $outputFileName]] \"]

      #
      # NOTE: Set the source code file name to the temporary source code
      #       file name we obtained from [file tempname] above.
      #
      lappend command [appendArgs \" [file nativename [file normalize \
          $sourceFileName]] \"]

      #
      # NOTE: First, write the specified string (containing C# code) to
      #       the temporary source code file.
      #
      writeFile $sourceFileName $string

      #
      # NOTE: Attempt to compile the temporary file as C# and capture the
      #       results into the variable provided by the caller.  Since the
      #       results are text, normalize line endings before extracting
      #       the compiler errors and/or warnings.
      #
      set local_results [runDotNetCSharpCommand $command]

      #
      # NOTE: Extract the compiler errors (which may be empty).
      #
      set errors [extractCSharpErrors $sourceFileName $local_results]

      #
      # NOTE: Extract the compiler warnings (which may be empty).
      #
      set warnings [extractCSharpWarnings $sourceFileName $local_results]

      #
      # NOTE: Prepare to transfer the "results" to the caller.
      #
      if {[string length $resultsVarName] > 0} then {
        upvar 1 $resultsVarName results
      }

      #
      # HACK: For backward compatibility with the results generated by
      #       the [compileViaCSharpCodeProvider] procedure, we must now
      #       set the results to an obviously fake opaque object handle
      #       that still matches the normal pattern.
      #
      set results System#CodeDom#Compiler#CompilerResults#0

      #
      # NOTE: It is assumed that no assembly was generated if there were
      #       any compiler errors.  Ignore all compiler warnings unless
      #       we are in strict mode.
      #
      if {[llength $errors] > 0 || \
          ($strict && [llength $warnings] > 0)} then {
        #
        # NOTE: Compilation of the assembly failed.
        #
        set code Error

        #
        # NOTE: Prepare to transfer error messages to the caller.
        #
        if {[string length $errorsVarName] > 0} then {
          upvar 1 $errorsVarName local_errors
        }

        #
        # NOTE: If there are compiler errors, add them to the list now.
        #
        if {[llength $errors] > 0} then {
          eval lappend local_errors $errors
        }

        #
        # NOTE: If there are compiler warnings, add them to the list now.
        #
        if {[llength $warnings] > 0} then {
          eval lappend local_errors $warnings
        }
      } else {
        #
        # NOTE: If the generated assembly was supposed to be loaded into
        #       memory, try to do that now.
        #
        if {$memory} then {
          object load -loadtype File $outputFileName
        }

        #
        # NOTE: Compilation of the assembly succeeded.
        #
        set code Ok
      }
    } finally {
      #
      # NOTE: Delete the temporary file name used to hold the source code.
      #
      if {[string length $sourceFileName] > 0 && \
          [file exists $sourceFileName]} then {
        catch {file delete $sourceFileName}
      }

      #
      # NOTE: Make sure the dummy temporary files are cleaned up.
      #
      if {[array exists tempName]} then {
        foreach tempFileName [array values tempName] {
          if {[file exists $tempFileName]} then {
            catch {file delete $tempFileName}
          }
        }
      }
    }

    #
    # HACK: *BREAKING CHANGE* If there is an output file name, return it
    #       as well; otherwise, just return success.
    #
    if {!$memory && [string length $outputFileName] > 0} then {
      #
      # NOTE: Return a two element list: the first element is the overall
      #       result and the second element is the output file name.
      #
      return [list $code $outputFileName]
    } else {
      #
      # NOTE: Return the overall result to the caller.
      #
      return $code
    }
  }

  #
  # NOTE: This procedure is used to dynamically compile arbitrary C# code
  #       from within a script.  This procedure was originally designed to
  #       be used by the test suite; however, it can be quite useful in
  #       non-test scripts as well.
  #
  proc compileCSharp {
          string memory symbols strict resultsVarName errorsVarName args } {
    if {[isDotNetCore]} then {
      return [uplevel 1 [list \
          compileViaDotNetCoreCSharp $string $memory $symbols $strict \
          $resultsVarName $errorsVarName] $args]
    } else {
      return [uplevel 1 [list \
          compileViaCSharpCodeProvider $string $memory $symbols $strict \
          $resultsVarName $errorsVarName] $args]
    }
  }

  #
  # NOTE: Provide the Eagle "C#" package to the interpreter.
  #
  package provide Eagle.CSharp \
    [expr {[isEagle] ? [info engine PatchLevel] : "1.0"}]
}
