@@ -97,34 +97,154 @@ fn parse_errors(raw: &str) -> Vec<CompileError> {
9797 errors
9898}
9999
100- /// Find the tectonic binary in PATH or known locations.
100+ /// Find the tectonic binary in PATH or known locations, auto-installing if needed .
101101fn find_tectonic ( ) -> Result < std:: path:: PathBuf > {
102- // Check PATH
103- if let Ok ( output) = Command :: new ( "which" ) . arg ( "tectonic" ) . output ( ) {
102+ if let Some ( path) = locate_tectonic ( ) {
103+ return Ok ( path) ;
104+ }
105+ eprintln ! ( "Tectonic not found. Installing automatically..." ) ;
106+ let dest = tectonic_managed_path ( ) ?;
107+ install_tectonic ( & dest) ?;
108+ Ok ( dest)
109+ }
110+
111+ /// Locate tectonic in PATH or known install locations without installing.
112+ fn locate_tectonic ( ) -> Option < std:: path:: PathBuf > {
113+ // Check PATH using platform-appropriate which/where
114+ #[ cfg( unix) ]
115+ let which_cmd = "which" ;
116+ #[ cfg( not( unix) ) ]
117+ let which_cmd = "where" ;
118+
119+ if let Ok ( output) = Command :: new ( which_cmd) . arg ( "tectonic" ) . output ( ) {
104120 if output. status . success ( ) {
105- let path = String :: from_utf8_lossy ( & output. stdout ) . trim ( ) . to_string ( ) ;
106- return Ok ( path. into ( ) ) ;
121+ let path = String :: from_utf8_lossy ( & output. stdout )
122+ . lines ( )
123+ . next ( )
124+ . unwrap_or ( "" )
125+ . trim ( )
126+ . to_string ( ) ;
127+ if !path. is_empty ( ) {
128+ return Some ( path. into ( ) ) ;
129+ }
107130 }
108131 }
109132
110- // Check known locations (including texforge-managed install)
111- for candidate in [
133+ // Check known locations
134+ [
112135 dirs:: home_dir ( ) . map ( |h| h. join ( ".texforge/bin/tectonic" ) ) ,
113136 dirs:: home_dir ( ) . map ( |h| h. join ( ".cargo/bin/tectonic" ) ) ,
114137 Some ( "/usr/local/bin/tectonic" . into ( ) ) ,
115138 Some ( "/opt/homebrew/bin/tectonic" . into ( ) ) ,
116139 ]
117140 . into_iter ( )
118141 . flatten ( )
142+ . find ( |p| p. exists ( ) )
143+ }
144+
145+ fn tectonic_managed_path ( ) -> Result < std:: path:: PathBuf > {
146+ dirs:: home_dir ( )
147+ . map ( |h| h. join ( ".texforge/bin/tectonic" ) )
148+ . ok_or_else ( || anyhow:: anyhow!( "Could not determine home directory" ) )
149+ }
150+
151+ /// Download and install tectonic to the given path.
152+ fn install_tectonic ( dest : & std:: path:: Path ) -> Result < ( ) > {
153+ let target = current_target ( ) ?;
154+ let version = "0.15.0" ;
155+ let ( filename, is_zip) = if target. contains ( "windows" ) {
156+ ( format ! ( "tectonic-{}-{}.zip" , version, target) , true )
157+ } else {
158+ ( format ! ( "tectonic-{}-{}.tar.gz" , version, target) , false )
159+ } ;
160+
161+ let url = format ! (
162+ "https://github.com/tectonic-typesetting/tectonic/releases/download/tectonic%40{}/{}" ,
163+ version, filename
164+ ) ;
165+
166+ eprintln ! ( "Downloading tectonic {}..." , version) ;
167+
168+ let response = reqwest:: blocking:: Client :: new ( )
169+ . get ( & url)
170+ . header ( "User-Agent" , "texforge" )
171+ . send ( )
172+ . context ( "Failed to download tectonic" ) ?;
173+
174+ if !response. status ( ) . is_success ( ) {
175+ anyhow:: bail!(
176+ "Failed to download tectonic: HTTP {}\n URL: {}" ,
177+ response. status( ) ,
178+ url
179+ ) ;
180+ }
181+
182+ let bytes = response. bytes ( ) ?;
183+
184+ if let Some ( parent) = dest. parent ( ) {
185+ std:: fs:: create_dir_all ( parent) ?;
186+ }
187+
188+ if is_zip {
189+ install_from_zip ( & bytes, dest) ?;
190+ } else {
191+ install_from_targz ( & bytes, dest) ?;
192+ }
193+
194+ #[ cfg( unix) ]
119195 {
120- if candidate. exists ( ) {
121- return Ok ( candidate) ;
196+ use std:: os:: unix:: fs:: PermissionsExt ;
197+ std:: fs:: set_permissions ( dest, std:: fs:: Permissions :: from_mode ( 0o755 ) ) ?;
198+ }
199+
200+ eprintln ! ( "✅ Tectonic installed to {}" , dest. display( ) ) ;
201+ Ok ( ( ) )
202+ }
203+
204+ fn install_from_targz ( bytes : & [ u8 ] , dest : & std:: path:: Path ) -> Result < ( ) > {
205+ let decoder = flate2:: read:: GzDecoder :: new ( bytes) ;
206+ let mut archive = tar:: Archive :: new ( decoder) ;
207+ for entry in archive. entries ( ) ? {
208+ let mut entry = entry?;
209+ let path = entry. path ( ) ?. to_string_lossy ( ) . to_string ( ) ;
210+ if path. ends_with ( "tectonic" ) || path == "tectonic" {
211+ std:: io:: copy ( & mut entry, & mut std:: fs:: File :: create ( dest) ?) ?;
212+ return Ok ( ( ) ) ;
122213 }
123214 }
215+ anyhow:: bail!( "tectonic binary not found in archive" )
216+ }
124217
125- anyhow:: bail!(
126- "Tectonic not found. Install everything with:\n \
127- \n curl -fsSL https://raw.githubusercontent.com/JheisonMB/texforge/main/install.sh | sh\n \
128- \n or install tectonic separately: cargo install tectonic"
129- ) ;
218+ fn install_from_zip ( bytes : & [ u8 ] , dest : & std:: path:: Path ) -> Result < ( ) > {
219+ let cursor = std:: io:: Cursor :: new ( bytes) ;
220+ let mut archive = zip:: ZipArchive :: new ( cursor) ?;
221+ for i in 0 ..archive. len ( ) {
222+ let mut file = archive. by_index ( i) ?;
223+ if file. name ( ) . ends_with ( "tectonic.exe" ) || file. name ( ) == "tectonic.exe" {
224+ std:: io:: copy ( & mut file, & mut std:: fs:: File :: create ( dest) ?) ?;
225+ return Ok ( ( ) ) ;
226+ }
227+ }
228+ anyhow:: bail!( "tectonic.exe not found in archive" )
229+ }
230+
231+ fn current_target ( ) -> Result < & ' static str > {
232+ #[ cfg( all( target_os = "linux" , target_arch = "x86_64" ) ) ]
233+ return Ok ( "x86_64-unknown-linux-musl" ) ;
234+ #[ cfg( all( target_os = "linux" , target_arch = "aarch64" ) ) ]
235+ return Ok ( "aarch64-unknown-linux-musl" ) ;
236+ #[ cfg( all( target_os = "macos" , target_arch = "x86_64" ) ) ]
237+ return Ok ( "x86_64-apple-darwin" ) ;
238+ #[ cfg( all( target_os = "macos" , target_arch = "aarch64" ) ) ]
239+ return Ok ( "aarch64-apple-darwin" ) ;
240+ #[ cfg( all( target_os = "windows" , target_arch = "x86_64" ) ) ]
241+ return Ok ( "x86_64-pc-windows-msvc" ) ;
242+ #[ cfg( not( any(
243+ all( target_os = "linux" , target_arch = "x86_64" ) ,
244+ all( target_os = "linux" , target_arch = "aarch64" ) ,
245+ all( target_os = "macos" , target_arch = "x86_64" ) ,
246+ all( target_os = "macos" , target_arch = "aarch64" ) ,
247+ all( target_os = "windows" , target_arch = "x86_64" ) ,
248+ ) ) ) ]
249+ anyhow:: bail!( "Unsupported platform for automatic tectonic installation" )
130250}
0 commit comments