diff --git a/lib/maker/make-pathway-tocs.lua b/lib/maker/make-pathway-tocs.lua index 3bbd0be4d45..440a8e15667 100755 --- a/lib/maker/make-pathway-tocs.lua +++ b/lib/maker/make-pathway-tocs.lua @@ -23,6 +23,13 @@ for _,course in ipairs(all_courses) do local this_course_lessons_file = course_dir .. '.cached/.workbook-lessons.rkt.kp' if not file_exists_p(this_course_lessons_file) then goto continue end local lesson_units = sread_file(this_course_lessons_file) + -- An empty or malformed .kp makes sread_file return a non-table (false on + -- empty/exhausted input). Skip the course rather than indexing into it. + if type(lesson_units) ~= 'table' then + io.stderr:write('WARNING: skipping pathway-toc for ' .. course .. + ' -- empty/unparseable ' .. this_course_lessons_file .. '\n') + goto continue + end o:write(' \"' .. course .. '\": [\n') for _,lunit in ipairs(lesson_units) do local unit_name = lunit[1] diff --git a/lib/maker/make-workbook-lessons-list.lua b/lib/maker/make-workbook-lessons-list.lua index dc8607c3d51..b69a35dd659 100755 --- a/lib/maker/make-workbook-lessons-list.lua +++ b/lib/maker/make-workbook-lessons-list.lua @@ -1,16 +1,13 @@ #! /usr/bin/env lua --- last modified 2026-03-10 +-- last modified 2026-06-15 local inf, outf, pl = ... -local i = io.open(inf, 'r') -local o = io.open(outf, 'w') - local current_unit = 'NO_UNIT' local current_unit_lessons = {} -local function write_unit() +local function write_unit(o) if #current_unit_lessons > 0 then o:write('( "' .. current_unit .. '"\n') for _,y in ipairs(current_unit_lessons) do @@ -26,28 +23,42 @@ if pl ~= 'pyret' and pl ~= 'none' then proglang = '-' .. pl end -o:write('(\n') - -for x0 in i:lines() do - local x = x0 - x = x:gsub('^%s+', '') - x = x:gsub(';.*', '') - x = x:gsub('%s+$', '') - if x == '' then goto continue end - if x:find('^%[') then - x = x:gsub('%[%s*(.-)%s*%]', '%1') - write_unit() - current_unit = x - current_unit_lessons = {} - else - x = x .. proglang - table.insert(current_unit_lessons, x) +local function get_all_lines(inf) + local the_lines = {} + for L in io.lines(inf) do + table.insert(the_lines, L) end - ::continue:: + return the_lines end -write_unit() -o:write(')\n') +local function write_all_units(inf, outf) + local the_lines = get_all_lines(inf) + local o = io.open(outf, 'w') + + o:write('(\n') + + for _,x0 in ipairs(the_lines) do + local x = x0 + x = x:gsub('^%s+', '') + x = x:gsub(';.*', '') + x = x:gsub('%s+$', '') + if x == '' then goto continue end + if x:find('^%[') then + x = x:gsub('%[%s*(.-)%s*%]', '%1') + write_unit(o) + current_unit = x + current_unit_lessons = {} + else + x = x .. proglang + table.insert(current_unit_lessons, x) + end + ::continue:: + end + write_unit(o) + + o:write(')\n') + + o:close() +end -i:close() -o:close() +write_all_units(inf, outf) diff --git a/lib/maker/massage-course.sh b/lib/maker/massage-course.sh index c0c22fb4780..1097b4bc809 100755 --- a/lib/maker/massage-course.sh +++ b/lib/maker/massage-course.sh @@ -72,12 +72,22 @@ for pl in $proglangs; do fi done + # Publish .workbook-lessons.rkt.kp atomically. make-workbook-lessons-list.lua + # opens its output with 'w' (truncates to 0 immediately), so a crash or + # interrupt would leave a 0-byte file that a later build's reader chokes on. + # Write a temp and mv it into place only on success; never leave it empty. + kp=.cached/.workbook-lessons.rkt.kp if test ! -f lesson-order.txt; then echo WARNING: No lesson-order.txt in pathway $targetpathway touch lesson-order.txt - touch .cached/.workbook-lessons.rkt.kp + printf '()\n' > "$kp" # valid empty s-expression, not a 0-byte file else - $TOPDIR/${MAKE_DIR}make-workbook-lessons-list.lua lesson-order.txt .cached/.workbook-lessons.rkt.kp $pl + if $TOPDIR/${MAKE_DIR}make-workbook-lessons-list.lua lesson-order.txt "$kp.tmp" $pl; then + mv -f "$kp.tmp" "$kp" + else + echo "ERROR: make-workbook-lessons-list failed for $targetpathway; keeping previous $kp" >&2 + rm -f "$kp.tmp" + fi fi cd .. diff --git a/lib/maker/sread.lua b/lib/maker/sread.lua index ae12a62e576..21e7cf7c98f 100644 --- a/lib/maker/sread.lua +++ b/lib/maker/sread.lua @@ -9,7 +9,7 @@ local function sread_line(i) while true do local c = i:read(1) - if c == '\r' or c == '\n' then + if c == nil or c == '\r' or c == '\n' then -- nil == EOF break end end @@ -18,7 +18,9 @@ end local function sread_block_comment(i) while true do local c = i:read(1) - if c == '|' then + if c == nil then -- EOF inside an unterminated block comment + break + elseif c == '|' then c = i:read(1) if c == '#' then break @@ -66,8 +68,8 @@ local function sread_atom(i) in_escape_p = true i:read(1) table.insert(result, c) - elseif c == ' ' or c == '\t' or c == '\n' or c == '\r' or c == '(' or c == '[' or c == ')' or c == ']' or c == ';' then - break + elseif c == nil or c == ' ' or c == '\t' or c == '\n' or c == '\r' or c == '(' or c == '[' or c == ')' or c == ']' or c == ';' then + break -- nil == EOF terminates the atom else i:read(1) table.insert(result, c) @@ -97,7 +99,9 @@ local function sread_string(i) local in_escape_p = false while true do local c = i:read(1) - if in_escape_p then + if c == nil then -- EOF inside an unterminated string + break + elseif in_escape_p then in_escape_p = false table.insert(result, c) elseif c == '\\' then @@ -116,7 +120,9 @@ function sread(i) sread_ignorespaces(i) local c = buf_peek_char(i) local result = false - if c == '(' or c == '[' then + if c == nil then -- EOF: no s-expression to read (empty/exhausted input) + result = false + elseif c == '(' or c == '[' then i:read(1) result = sread_list(i) elseif c == ')' or c == ']' then