2828WHITE = "\033 [97m"
2929YELLOW = "\033 [93m"
3030RESET = "\033 [0m"
31+ BOLD = "\033 [1m"
3132FEINT = "\033 [2m"
3233ITALIC = "\033 [3m"
3334BLINK = "\033 [6m"
3839
3940@lru_cache (maxsize = None )
4041def aoc_available_puzzles (
41- year : int | None = None , seconds : float | None = None
42+ year : t . Optional [ int ] = None , seconds : t . Optional [ float ] = None
4243) -> t .Union [dict [int , list [int ]], list [int ]]:
4344 """
4445 Returns a dict of available puzzles by year or the list of available puzzles for the given year.
@@ -71,7 +72,16 @@ def aoc_available_puzzles(
7172 return puzzles
7273
7374
74- AOC_TARGET_DIR = os .environ .get ("AOC_TARGET_DIR" , "target" )
75+ class Env :
76+ """
77+ Variables that can be overridden by environment variables.
78+ """
79+
80+ AOC_TARGET_DIR = os .environ .get ("AOC_TARGET_DIR" , "target" )
81+ CARGO_TARGET_DIR = os .environ .get ("CARGO_TARGET_DIR" , "target" )
82+ CC = os .environ .get ("CC" , "cc" )
83+ CXX = os .environ .get ("CXX" , "c++" )
84+
7585
7686LANGUAGES = {
7787 "Rust" : "src/year{year}/day{day}/day{day}.rs" ,
@@ -82,7 +92,7 @@ def aoc_available_puzzles(
8292 "JavaScript" : "src/year{year}/day{day}/day{day}.js" ,
8393 "Ruby" : "src/year{year}/day{day}/day{day}.rb" ,
8494 "Perl" : "src/year{year}/day{day}/day{day}.pl" ,
85- "Bash" : "src/year{year}/day{day}/day{day}.sh " ,
95+ "Bash" : "src/year{year}/day{day}/day{day}.bash " ,
8696 "Java" : "{AOC_TARGET_DIR}/build/year{year}/day{day}.class" ,
8797 "Go" : "{AOC_TARGET_DIR}/build/year{year}/day{day}_go" ,
8898 "C#" : "{AOC_TARGET_DIR}/build/year{year}/day{day}_cs.exe" ,
@@ -113,8 +123,96 @@ def aoc_available_puzzles(
113123 "Ruby" : {"Ruby" : "ruby" },
114124 "Perl" : {"Perl" : "perl" },
115125 "Tcl" : {"Tcl" : "tclsh" },
126+ "Bash" : {"Bash" : "bash" },
116127}
117128
129+ LANGUAGES_VERSIONS = {
130+ "Rust" : "rustc --version" ,
131+ "Go" : "go version" ,
132+ "C" : "{CC} --version" ,
133+ "C++" : "{CXX} --version" ,
134+ "Swift" : "swiftc -version" ,
135+ "Java" : "javac -version" ,
136+ "C#" : "mcs --version" ,
137+ "Python" : "{interpreter} -VV" ,
138+ "Ruby" : "{interpreter} --version" ,
139+ "Lua" : "{interpreter} -v" ,
140+ "JavaScript" : {"NodeJS" : "{interpreter} -v" },
141+ "Perl" : "{interpreter} -v" ,
142+ "Tcl" : "echo 'puts [info patchlevel]' | {interpreter}" ,
143+ "Bash" : "{interpreter} --version" ,
144+ }
145+
146+
147+ def get_language_version (puzzle_lang : str ) -> str :
148+ for language , variants in INTERPRETERS .items ():
149+ # language = Python
150+ # variants = Py3.11, Py3.12, ...
151+ for variant , interpreters in variants .items ():
152+ if variant == puzzle_lang :
153+ # language should be in the "show version" dict
154+ if language not in LANGUAGES_VERSIONS :
155+ continue
156+
157+ # interpreter could be a string or an iterable of strings
158+ if isinstance (interpreters , str ):
159+ interpreters = (interpreters ,)
160+
161+ # get the first available interpreter
162+ for interpreter in interpreters :
163+ interpreter = interpreter .format (** vars (Env ))
164+ if os .path .isfile (shutil .which (interpreter )):
165+ break
166+ else :
167+ return ""
168+
169+ cmd = LANGUAGES_VERSIONS [language ]
170+ if isinstance (cmd , dict ):
171+ for v , cmd_v in cmd .items ():
172+ if v == variant :
173+ cmd = cmd_v
174+ break
175+ else :
176+ # variant has no "show version" command
177+ return ""
178+
179+ cmd = cmd .format (interpreter = interpreter )
180+ cmd = cmd .format (** vars (Env ))
181+ try :
182+ lang_version = (
183+ subprocess .check_output (
184+ cmd ,
185+ shell = True ,
186+ stderr = subprocess .DEVNULL ,
187+ )
188+ .decode ()
189+ .strip ()
190+ .splitlines ()[0 ]
191+ )
192+ return lang_version
193+ except Exception :
194+ return ""
195+
196+ if puzzle_lang in LANGUAGES_VERSIONS :
197+ cmd = LANGUAGES_VERSIONS [puzzle_lang ]
198+ cmd = cmd .format (** vars (Env ))
199+ try :
200+ lang_version = (
201+ subprocess .check_output (
202+ cmd ,
203+ shell = True ,
204+ stderr = subprocess .DEVNULL ,
205+ )
206+ .decode ()
207+ .strip ()
208+ .splitlines ()[0 ]
209+ )
210+ return lang_version
211+ except Exception :
212+ return ""
213+
214+ return ""
215+
118216
119217def get_cache (cache_file : Path = None ):
120218 """Retrieve the cache instance from memory or load it from disk."""
@@ -211,14 +309,15 @@ def run(
211309 nb_expected : int ,
212310 year : int ,
213311 day : int ,
312+ quiet : bool ,
214313) -> t .Dict [str , t .Any ]:
215314 """
216315 TODO
217316 """
218317
219318 if lang == "Rust" :
220319 cmd = []
221- target = os . environ . get ( " CARGO_TARGET_DIR" , "target" )
320+ target = Env . CARGO_TARGET_DIR
222321
223322 f = Path (f"{ prog .parent } /{ prog .stem } /{ target } /release/{ prog .stem } " )
224323 if f .is_file ():
@@ -248,12 +347,14 @@ def run(
248347 cmd .append (file .absolute ().as_posix ())
249348 cmd .append ("--elapsed" )
250349
251- env = os .environ
350+ env = os .environ . copy ()
252351 env ["NO_COLOR" ] = "1"
253352 cmdline = " " .join (map (str , cmd ))
254353 cmdline = cmdline .replace (Path (__file__ ).parent .parent .as_posix () + "/" , "" )
255354 cmdline = cmdline .replace (Path .home ().as_posix (), "~" )
256- print (f"{ FEINT } { cmdline } { RESET } " , end = TRANSIENT )
355+
356+ if not quiet :
357+ print (f"{ FEINT } { cmdline } { RESET } " , end = TRANSIENT )
257358
258359 elapsed_measurement = "process"
259360 start = time .time_ns ()
@@ -281,7 +382,7 @@ def run(
281382 elif elapsed2 .endswith ("s" ):
282383 elapsed = int (10 ** 9 * float (elapsed2 .removesuffix ("s" )))
283384 else :
284- print (elapsed , elapsed2 , "unknown suffix" )
385+ print (elapsed , elapsed2 , "unknown time suffix" )
285386 exit ()
286387
287388 elapsed_measurement = "internal"
@@ -303,7 +404,7 @@ def run(
303404
304405 result = {"elapsed" : elapsed , "status" : status , "answers" : answers }
305406
306- run_log = Path (AOC_TARGET_DIR ) / "run.log"
407+ run_log = Path (Env . AOC_TARGET_DIR ) / "run.log"
307408
308409 with run_log .open ("at" ) as f :
309410 line = f"{ datetime .now ()} { lang } { cmd } { elapsed / 1e9 } { elapsed_measurement } { status } '{ answers } '"
@@ -317,7 +418,7 @@ def make(year: int, source: Path, dest: Path, language: str, disable_language: t
317418 if not source .is_file ():
318419 return
319420
320- build_dir = Path (f"{ AOC_TARGET_DIR } /build/year{ year } " )
421+ build_dir = Path (f"{ Env . AOC_TARGET_DIR } /build/year{ year } " )
321422 build_dir .mkdir (parents = True , exist_ok = True )
322423
323424 output = build_dir / dest
@@ -326,10 +427,10 @@ def make(year: int, source: Path, dest: Path, language: str, disable_language: t
326427 return
327428
328429 if language == "C" :
329- cmd = "cc -std=c11"
430+ cmd = "{CC} -std=c11" . format ( CC = Env . CC )
330431 cmdline = f"{ cmd } -o { output } -Wall -Wextra -Werror -O3 -DSTANDALONE -I{ source .parent } { source } "
331432 elif language == "C++" :
332- cmd = "c++ -std=c++23"
433+ cmd = "{CXX} -std=c++23" . format ( CXX = Env . CXX )
333434 cmdline = f"{ cmd } -o { output } -Wall -Wextra -Werror -O3 -DSTANDALONE -I{ source .parent } { source } "
334435 elif language == "Java" :
335436 cmdline = f"javac -d { build_dir } { source } "
@@ -497,6 +598,7 @@ def run_day(
497598 dry_run : bool ,
498599 terminal_columns : int ,
499600 wait : float ,
601+ quiet : bool ,
500602):
501603 elapsed = defaultdict (list )
502604
@@ -519,7 +621,7 @@ def run_day(
519621 results = set ()
520622
521623 for lang , (pattern , interpreter ) in languages .items ():
522- prog = Path (pattern .format (year = year , day = mday , AOC_TARGET_DIR = AOC_TARGET_DIR ))
624+ prog = Path (pattern .format (year = year , day = mday , AOC_TARGET_DIR = Env . AOC_TARGET_DIR ))
523625 key = ":" .join (map (str , (year , day , crc , prog , lang .lower ())))
524626
525627 if not prog .is_file ():
@@ -540,20 +642,21 @@ def run_day(
540642 if not in_cache and not dry_run :
541643 nb_expected = 1 if day == 25 else 2
542644
543- e = run (prog , lang , interpreter , file , day_answers .get (crc ), nb_expected , year , day )
645+ e = run (prog , lang , interpreter , file , day_answers .get (crc ), nb_expected , year , day , quiet )
544646
545647 if e :
546648 update_cache (key , prog , "solutions" , e )
547649
548650 if wait is not None :
549- print (f"{ CR } { CLEAR_EOL } waiting { wait :g} s..." , end = "" )
651+ if not quiet :
652+ print (f"{ CR } { CLEAR_EOL } waiting { wait :g} s..." , end = "" )
550653 time .sleep (wait )
551654
552655 if not e :
553656 continue
554657
555658 if (e ["status" ] == "unknown" and day_answers .get (crc )) or e ["status" ] in ("error" , "failed" ):
556- resolve_script = Path (AOC_TARGET_DIR ) / f"resolve_{ e ['status' ]} .sh"
659+ resolve_script = Path (Env . AOC_TARGET_DIR ) / f"resolve_{ e ['status' ]} .sh"
557660
558661 if not globals ().get (resolve_script ):
559662 with resolve_script .open ("wt" ) as f :
@@ -582,7 +685,9 @@ def run_day(
582685 f" { GRAY } { '☽' if in_cache else ' ' } "
583686 )
584687
585- if terminal_columns < 60 :
688+ if quiet :
689+ pass
690+ elif terminal_columns < 60 :
586691 print (line )
587692 elif terminal_columns < 130 :
588693 print (line , f"{ status_color } { str (answers )} { RESET } " )
@@ -621,7 +726,7 @@ def lookup(interpreter: str) -> str:
621726
622727 if "{" in interpreter :
623728 # interpreter = interpreter.format_map(globals())
624- interpreter = interpreter .format (AOC_TARGET_DIR = AOC_TARGET_DIR )
729+ interpreter = interpreter .format (AOC_TARGET_DIR = Env . AOC_TARGET_DIR )
625730
626731 if "/" not in interpreter and "\\ " not in interpreter :
627732 interpreter = shutil .which (interpreter )
@@ -749,6 +854,7 @@ def main():
749854 parser = argparse .ArgumentParser (formatter_class = lambda prog : argparse .HelpFormatter (prog , max_help_position = 28 ))
750855
751856 parser .add_argument ("-v" , "--verbose" , action = "store_true" , help = "verbose" )
857+ parser .add_argument ("-q" , "--quiet" , action = "store_true" , help = "quiet" )
752858 parser .add_argument ("--cache" , type = Path , help = "cache database" )
753859 parser .add_argument ("--working-dir" , type = Path , help = argparse .SUPPRESS )
754860
@@ -856,7 +962,7 @@ def main():
856962 # load inputs and answers
857963 inputs , answers = load_data (filter_year , args .filter_user , args .exclude , args .verified )
858964
859- for script in Path (AOC_TARGET_DIR ).glob ("resolve_*.sh" ):
965+ for script in Path (Env . AOC_TARGET_DIR ).glob ("resolve_*.sh" ):
860966 script .unlink ()
861967
862968 # here we go!
@@ -880,7 +986,7 @@ def main():
880986 day_solutions += Path (f"src/year{ year } " ).glob (f"day{ day } _*" )
881987
882988 for mday in day_solutions :
883- if prev_shown_year != year :
989+ if not args . quiet and prev_shown_year != year :
884990 if prev_shown_year != 0 :
885991 line = (
886992 "==========================" # prefix
@@ -905,17 +1011,19 @@ def main():
9051011 args .dry_run ,
9061012 terminal_columns ,
9071013 args .wait ,
1014+ args .quiet ,
9081015 )
9091016
9101017 if elapsed :
911- if nb_samples > 1 :
912- print (
913- f"{ CR } { CLEAR_EOL } --> " ,
914- " | " .join ((f"{ lang } : { t :.3f} s" for lang , t in elapsed .items ())),
915- f"{ FEINT } ({ nb_samples } input{ 's' if nb_samples > 1 else '' } ){ RESET } " ,
916- )
917- else :
918- print (end = f"{ CR } { CLEAR_EOL } " )
1018+ if not args .quiet :
1019+ if nb_samples > 1 :
1020+ print (
1021+ f"{ CR } { CLEAR_EOL } --> " ,
1022+ " | " .join ((f"{ lang } : { t :.3f} s" for lang , t in elapsed .items ())),
1023+ f"{ FEINT } ({ nb_samples } input{ 's' if nb_samples > 1 else '' } ){ RESET } " ,
1024+ )
1025+ else :
1026+ print (end = f"{ CR } { CLEAR_EOL } " )
9191027
9201028 for lang , e in elapsed .items ():
9211029 stats_elapsed [year , day , mday , lang ] = (e , nb_samples )
@@ -945,22 +1053,24 @@ def main():
9451053 if average < 0.001 :
9461054 average = f"{ average :10.6f} s"
9471055 else :
948- average = f"{ average :7.3f} s"
949-
950- lines .append (
951- (
952- t ,
953- f"{ YELLOW } { lang :<10} { RESET } "
954- f" : { GREEN } { t :9.3f} s{ RESET } for { WHITE } { n :3} { RESET } puzzle{ 's' if n > 1 else ' ' } ,"
955- f" average: { GREEN } { average } { RESET } " ,
956- )
1056+ average = f"{ average :7.3f} s "
1057+
1058+ stats = (
1059+ f"{ YELLOW } { lang :<10} { RESET } "
1060+ f" : { GREEN } { t :9.3f} s{ RESET } for { WHITE } { n :3} { RESET } puzzle{ 's' if n > 1 else ' ' } ,"
1061+ f" average: { GREEN } { average } { RESET } "
9571062 )
1063+ ver = get_language_version (lang )
1064+ if ver :
1065+ stats += f" ∞ { ITALIC } { ver } { RESET } "
1066+
1067+ lines .append ((t , stats ))
9581068 total_time += t
9591069 nb_solutions += n
9601070
961- print ()
962- print (" ELAPSED TIME:" )
963- print ("\n " .join (map (itemgetter (1 ), sorted (lines ))))
1071+ print (f" { CR } { CLEAR_EOL } " )
1072+ print (f" { BOLD } ELAPSED TIME:{ RESET } " )
1073+ print ("\n " .join (map (itemgetter (1 ), sorted (lines , key = itemgetter ( 1 ) ))))
9641074 print (
9651075 "total "
9661076 f" : { GREEN } { total_time :9.3f} s{ RESET } "
@@ -1009,11 +1119,11 @@ def main():
10091119 f"{ YELLOW } { lang1 :<7} { RESET } "
10101120 f" vs. { YELLOW } { lang2 :<7} { RESET } :"
10111121 f" { GREEN } { t1 / n :7.3f} s{ RESET } vs. { GREEN } { t2 / n :7.3f} s{ RESET } "
1012- f" (x { faster :4 .1f} faster) on { WHITE } { n :3} { RESET } puzzle{ 's' if n > 1 else '' } " ,
1122+ f" (x { faster :5 .1f} faster) on { WHITE } { n :3} { RESET } puzzle{ 's' if n > 1 else '' } " ,
10131123 )
10141124 )
10151125 print ()
1016- print (" LANGUAGES COMPARISON:" )
1126+ print (f" { BOLD } LANGUAGES COMPARISON:{ RESET } " )
10171127 print ("\n " .join (map (itemgetter (2 ), sorted (lines ))))
10181128
10191129 elif len (languages ) > 1 :
@@ -1025,7 +1135,7 @@ def main():
10251135 for problem in problems :
10261136 print (problem )
10271137
1028- for i , f in enumerate (Path (AOC_TARGET_DIR ).glob ("resolve_*.sh" )):
1138+ for i , f in enumerate (Path (Env . AOC_TARGET_DIR ).glob ("resolve_*.sh" )):
10291139 if i == 0 :
10301140 print ("\n Fix errors then run:" )
10311141 print (f" { RED } { f } { RESET } " )
0 commit comments