Skip to content

Commit 9db00d1

Browse files
Embed tclreadline Tcl scripts into the silisizer binary
The interactive prompt previously searched the filesystem for tclreadlineInit.tcl/Setup.tcl at run time and printed "tclreadlineInit.tcl not found; interactive line editing disabled" whenever the bundled scripts were missing or relocated. Vendor the tclreadline v2.4.1 Setup + Completer scripts under src/tcl/, encode them into the executable via OpenSTA's TclEncode.tcl (the same mechanism used for tcl_inits), and evaluate them at startup. This removes the external file dependency entirely. The vendored Setup only re-provides the tclreadline package if the statically-linked C library has not already done so, avoiding a version-conflict error (the v2.4.1 release reports 2.4.0). Drop the now-redundant script-bundling steps from the release workflow; only libtclreadline itself still needs bundling. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent a51e2b5 commit 9db00d1

6 files changed

Lines changed: 6951 additions & 51 deletions

File tree

.github/workflows/release.yml

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,8 @@ jobs:
107107
TCL_LIB_DIR=$(find /usr/lib /usr/local/lib -maxdepth 1 -name "tcl8.6" -type d 2>/dev/null | head -1)
108108
[ -d "$TCL_LIB_DIR" ] && cp -r "$TCL_LIB_DIR" $STAGE/lib/tcl8.6
109109
110-
# Bundle tclreadline scripts (tclreadlineInit.tcl etc.) so the
111-
# interactive prompt works in the relocated install.
112-
for d in /usr/local/lib/tclreadline*; do
113-
[ -d "$d" ] && cp -r "$d" "$STAGE/lib/"
114-
done
110+
# The tclreadline Tcl scripts are embedded in the silisizer
111+
# binary, so only the shared library (bundled above) is needed.
115112
116113
for f in "$STAGE"/bin/*; do
117114
[ -f "$f" ] && patchelf --set-rpath "\$ORIGIN/../lib" "$f" 2>/dev/null || true
@@ -289,11 +286,8 @@ jobs:
289286
echo "TCL_LIB_DIR: $TCL_LIB_DIR"
290287
[ -d "$TCL_LIB_DIR" ] && cp -a "$TCL_LIB_DIR" "$STAGE/lib/tcl8.6"
291288
292-
# Bundle tclreadline scripts (tclreadlineInit.tcl etc.) so the
293-
# interactive prompt works in the relocated install.
294-
for d in /usr/local/lib/tclreadline*; do
295-
[ -d "$d" ] && cp -a "$d" "$STAGE/lib/"
296-
done
289+
# The tclreadline Tcl scripts are embedded in the silisizer
290+
# binary, so only the shared library (bundled above) is needed.
297291
298292
for f in "$STAGE"/bin/*; do
299293
[ -f "$f" ] && patchelf --set-rpath "\$ORIGIN/../lib" "$f" 2>/dev/null || true
@@ -432,11 +426,8 @@ jobs:
432426
# Bundle Tcl 8.6 library scripts
433427
cp -r "$(brew --prefix tcl-tk@8)/lib/tcl8.6" $STAGE/lib/tcl8.6
434428
435-
# Bundle tclreadline scripts (tclreadlineInit.tcl etc.) so the
436-
# interactive prompt works in the relocated install.
437-
for d in /usr/local/lib/tclreadline*; do
438-
[ -d "$d" ] && cp -R "$d" "$STAGE/lib/"
439-
done
429+
# The tclreadline Tcl scripts are embedded in the silisizer binary,
430+
# so only the shared library (bundled above) is needed.
440431
441432
# install_name_tool invalidates the code signature; arm64 macOS
442433
# refuses to run binaries/dylibs with a stale signature, so re-sign

CMakeLists.txt

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,31 @@ if (WIN32)
152152
add_compile_definitions(WIN32_LEAN_AND_MEAN)
153153
endif()
154154

155+
# Embed the tclreadline Tcl scripts (interactive Loop + command completer) into
156+
# the executable so the interactive prompt has no external file dependency.
157+
# They are encoded into a C string array (named tclreadline_inits in namespace
158+
# sta) exactly like OpenSTA's own tcl_inits, then unencoded and evaluated at
159+
# startup. This removes the need to locate tclreadlineInit.tcl/Setup.tcl on disk.
160+
set(SILISIZER_TCLRL_INIT "")
161+
if (TCL_READLINE)
162+
set(SILISIZER_TCLRL_TCL_FILES
163+
${PROJECT_SOURCE_DIR}/src/tcl/tclreadlineSetup.tcl
164+
${PROJECT_SOURCE_DIR}/src/tcl/tclreadlineCompleter.tcl
165+
${PROJECT_SOURCE_DIR}/src/tcl/tclreadlineEmbedInit.tcl
166+
)
167+
set(SILISIZER_TCLRL_INIT ${CMAKE_BINARY_DIR}/SilisizerTclReadlineInitVar.cc)
168+
find_program(TCLSH_EXECUTABLE NAMES tclsh tclsh8.6 tclsh8.7 tclsh9.0 tclsh90)
169+
if (TCLSH_EXECUTABLE)
170+
set(SILISIZER_TCL_ENCODE_CMD ${TCLSH_EXECUTABLE} ${OPENSTA_HOME}/etc/TclEncode.tcl)
171+
else()
172+
set(SILISIZER_TCL_ENCODE_CMD ${OPENSTA_HOME}/etc/TclEncode.tcl)
173+
endif()
174+
add_custom_command(OUTPUT ${SILISIZER_TCLRL_INIT}
175+
COMMAND ${SILISIZER_TCL_ENCODE_CMD} ${SILISIZER_TCLRL_INIT} tclreadline_inits ${SILISIZER_TCLRL_TCL_FILES}
176+
DEPENDS ${SILISIZER_TCLRL_TCL_FILES} ${OPENSTA_HOME}/etc/TclEncode.tcl
177+
)
178+
endif()
179+
155180
set(silisizer_SRC
156181
${PROJECT_SOURCE_DIR}/src/Silisizer.cpp
157182
${CMAKE_BINARY_DIR}/Silisizer_wrap.cc
@@ -177,7 +202,7 @@ target_include_directories(silisizer PUBLIC
177202
${TCL_HEADER_DIR}
178203
$<INSTALL_INTERFACE:include>)
179204

180-
add_executable(silisizer-bin ${PROJECT_SOURCE_DIR}/src/main.cpp)
205+
add_executable(silisizer-bin ${PROJECT_SOURCE_DIR}/src/main.cpp ${SILISIZER_TCLRL_INIT})
181206
set_target_properties(silisizer-bin PROPERTIES OUTPUT_NAME silisizer)
182207

183208
if (MSVC OR WIN32)

src/main.cpp

Lines changed: 21 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ extern int Sta_Init(Tcl_Interp *interp);
8686
namespace sta {
8787
// extern const char *silisizer_tcl_inits[];
8888
extern const char *tcl_inits[];
89+
#if TCL_READLINE
90+
// tclreadline Tcl scripts (Setup + Completer + completer hookup) encoded into
91+
// the executable by TclEncode.tcl (see SilisizerTclReadlineInitVar.cc).
92+
extern const char *tclreadline_inits[];
93+
#endif
8994
} // namespace sta
9095

9196
// "Arguments" passed to staTclAppInit.
@@ -157,47 +162,28 @@ static int silisizerTclAppInit(Tcl_Interp *interp) {
157162
}
158163

159164
#if TCL_READLINE
160-
// Initialize the C side of tclreadline. The library is linked into this
161-
// binary, so we also register it as a static package and (below) tell the Tcl
162-
// init script to skip its own dlopen of libtclreadline. This is what lets
163-
// tclreadline keep working in a relocated/packaged install where the shared
164-
// library is only reachable via the executable's RPATH.
165+
// Initialize the C side of tclreadline and register it as a static package.
166+
// The library is linked into this binary (reachable via the executable's
167+
// RPATH in a packaged install), so the Tcl scripts below skip their own
168+
// dlopen of libtclreadline.
165169
if (Tclreadline_Init(interp) == TCL_ERROR)
166170
return TCL_ERROR;
167171
Tcl_StaticPackage(interp, "tclreadline", Tclreadline_Init, Tclreadline_SafeInit);
168172

169-
// Locate and source tclreadlineInit.tcl (which sources tclreadlineSetup.tcl
170-
// and thereby defines ::tclreadline::Loop). The compile-time TCLRL_LIBRARY
171-
// path does not exist once the build is packaged and extracted elsewhere, so
172-
// search next to the executable first (the bundled copy), then fall back to
173-
// the build-time path, $TCL_LIBRARY and a few standard locations.
173+
// The tclreadline Tcl scripts (the interactive ::tclreadline::Loop defined in
174+
// tclreadlineSetup.tcl plus the command completer in tclreadlineCompleter.tcl)
175+
// are compiled directly into this binary as sta::tclreadline_inits, so there
176+
// is no external tclreadlineInit.tcl/Setup.tcl to locate at run time. Decode
177+
// and evaluate them now; on failure we fall back to Tcl_Main's plain REPL.
174178
//
175179
// Tclreadline_Init() above already created the read-only ::tclreadline::library
176-
// variable, so tclreadlineInit.tcl's own "if {![info exists
177-
// tclreadline::library]}" guard skips its dlopen of libtclreadline. That keeps
178-
// tclreadline working in a relocated install where the shared library is only
179-
// reachable via the executable's RPATH. (Do not try to set that variable here:
180-
// it is read-only and assigning to it aborts this whole script.)
181-
static const char *tclreadline_loader =
182-
"namespace eval ::tclreadline {}\n"
183-
"proc ::tclreadline::_locate_init {} {\n"
184-
" set bases [list [file join [file dirname [info nameofexecutable]] .. lib] {" TCLRL_LIBRARY "} /usr/local/lib /usr/lib]\n"
185-
" if {[info exists ::env(TCL_LIBRARY)]} { lappend bases $::env(TCL_LIBRARY) [file join $::env(TCL_LIBRARY) ..] }\n"
186-
" foreach base $bases {\n"
187-
" set cands [list [file join $base tclreadlineInit.tcl]]\n"
188-
" foreach g [lsort -decreasing [glob -nocomplain [file join $base tclreadline* tclreadlineInit.tcl]]] { lappend cands $g }\n"
189-
" foreach cand $cands { if {[file exists $cand]} { return $cand } }\n"
190-
" }\n"
191-
" return {}\n"
192-
"}\n"
193-
"set ::tclreadline::_init_script [::tclreadline::_locate_init]\n"
194-
"if {$::tclreadline::_init_script ne {}} {\n"
195-
" if {[catch {source $::tclreadline::_init_script} ::tclreadline::_err]} { puts stderr \"tclreadline: failed to load $::tclreadline::_init_script: $::tclreadline::_err\" }\n"
196-
"} else {\n"
197-
" puts stderr {tclreadline: tclreadlineInit.tcl not found; interactive line editing disabled}\n"
198-
"}\n";
199-
if (Tcl_Eval(interp, tclreadline_loader) != TCL_OK)
200-
fprintf(stderr, "tclreadline: setup failed: %s\n", Tcl_GetStringResult(interp));
180+
// variable, so the scripts skip their own dlopen of libtclreadline (which is
181+
// statically linked here anyway).
182+
char *tclreadline_init = sta::unencode(sta::tclreadline_inits);
183+
if (Tcl_Eval(interp, tclreadline_init) != TCL_OK)
184+
fprintf(stderr, "tclreadline: embedded init failed: %s\n",
185+
Tcl_GetStringResult(interp));
186+
delete [] tclreadline_init;
201187
#endif
202188

203189
// Define swig commands.

0 commit comments

Comments
 (0)