To update the build number(s) and versions for the intools.
§1. The build-numbers file. The scheme here is that each project can optionally contain a UTF-8 encoded
text file called versions.txt
, which lists all of its version history;
a line of that file corresponds to a "version". Out of these versions, one
must be marked as the current version.
typedef struct project {
struct text_stream *sync_line;
struct project *sync_to;
int manual_updating;
struct text_stream *web;
struct filename *contents_file;
struct filename *versions_file;
struct linked_list *versions; of version
struct version *current_version;
struct text_stream *purpose;
struct text_stream *conts;
int next_is_version;
MEMORY_MANAGEMENT
} project;
typedef struct version {
struct text_stream *name;
struct text_stream *number;
struct text_stream *build_code;
struct text_stream *date;
struct text_stream *notes;
MEMORY_MANAGEMENT
} version;
The structure project is accessed in 2/rw and here.
The structure version is accessed in 2/pc, 2/rw and here.
§2. And here we take a filename or pathname, which might be to a web, with or without a versions file; or to an extension; or to a website template; or to the original Inform 6 source code. These all store their version numbering differently, so we need code which is something of a Swiss army knife.
project *Inversion::read(text_stream *web, int silently) { project *P; LOOP_OVER(P, project) if (Str::eq(web, P->web)) return P; P = CREATE(project); P->sync_line = Str::new(); P->sync_to = NULL; P->manual_updating = TRUE; P->web = Str::duplicate(web); P->versions = NEW_LINKED_LIST(version); P->current_version = NULL; P->conts = Str::new(); P->purpose = Str::new(); P->next_is_version = FALSE; if (Str::ends_with_wide_string(web, L".i7x")) { P->versions_file = NULL; P->contents_file = NULL; <Read in the extension file 2.2>; } else { P->versions_file = Filenames::in_folder(Pathnames::from_text(web), I"versions.txt"); P->contents_file = Filenames::in_folder(Pathnames::from_text(web), I"Contents.w"); if (TextFiles::exists(P->contents_file)) { <Read in the contents file 2.3>; if (TextFiles::exists(P->versions_file) == FALSE) <Read version from the contents file 2.4>; } if (TextFiles::exists(P->versions_file)) <Read in the versions file 2.5>; filename *I6_vn = Filenames::in_folder( Pathnames::subfolder(Pathnames::from_text(web), I"inform6"), I"header.h"); if (TextFiles::exists(I6_vn)) <Read in I6 source header file 2.6>; filename *template_vn = Filenames::in_folder(Pathnames::from_text(web), I"(manifest).txt"); if (TextFiles::exists(template_vn)) <Read in template manifest file 2.7>; filename *rmt_vn = Filenames::in_folder(Pathnames::from_text(web), I"README.txt"); if (TextFiles::exists(rmt_vn)) <Read in README file 2.8>; rmt_vn = Filenames::in_folder(Pathnames::from_text(web), I"README.md"); if (TextFiles::exists(rmt_vn)) <Read in README file 2.8>; } <Print the current version number 2.1>; return P; }
The function Inversion::read is used in §3.2, §8, 2/rw (§4).
§2.1.
<Print the current version number 2.1> =
if ((P->current_version) && (!silently)) PRINT("%S: %S %S (build %S)\n", web, P->current_version->name, P->current_version->number, P->current_version->build_code);
This code is used in §2.
§2.2.
<Read in the extension file 2.2> =
TextFiles::read(Filenames::from_text(web), FALSE, "unable to read extension", TRUE, &Inversion::extension_harvester, NULL, P);
This code is used in §2.
§2.3.
<Read in the contents file 2.3> =
TextFiles::read(P->contents_file, FALSE, "unable to read contents section", TRUE, &Inversion::contents_harvester, NULL, P);
This code is used in §2.
§2.4.
<Read version from the contents file 2.4> =
TextFiles::read(P->contents_file, FALSE, "unable to read contents section", TRUE, &Inversion::contents_version_harvester, NULL, P);
This code is used in §2.
§2.5.
<Read in the versions file 2.5> =
TextFiles::read(P->versions_file, FALSE, "unable to read roster of version numbers", TRUE, &Inversion::version_harvester, NULL, P);
This code is used in §2.
§2.6.
<Read in I6 source header file 2.6> =
TextFiles::read(I6_vn, FALSE, "unable to read header file from I6 source", TRUE, &Inversion::header_harvester, NULL, P);
This code is used in §2.
§2.7.
<Read in template manifest file 2.7> =
TextFiles::read(template_vn, FALSE, "unable to read manifest file from website template", TRUE, &Inversion::template_harvester, NULL, P);
This code is used in §2.
§2.8.
<Read in README file 2.8> =
TextFiles::read(rmt_vn, FALSE, "unable to read manifest file from website template", TRUE, &Inversion::readme_harvester, NULL, P);
This code is used in §2 (twice).
§3. The format for the contents section of a web is documented in Inweb.
void Inversion::extension_harvester(text_stream *text, text_file_position *tfp, void *state) { project *P = (project *) state; match_results mr = Regexp::create_mr(); if (Str::len(text) == 0) return; if (Regexp::match(&mr, text, L" *Version (%c*?) of %c*begins here. *")) { <Ensure a current version exists 3.6>; P->current_version->number = Str::duplicate(mr.exp[0]); } Regexp::dispose_of(&mr); }
The function Inversion::extension_harvester is used in §2.2.
§3.1. The format for the contents section of a web is documented in Inweb.
void Inversion::contents_harvester(text_stream *text, text_file_position *tfp, void *state) { project *P = (project *) state; match_results mr = Regexp::create_mr(); if (Str::len(text) == 0) return; if (Regexp::match(&mr, text, L" *Purpose: *(%c*?) *")) { P->purpose = Str::duplicate(mr.exp[0]); } Regexp::dispose_of(&mr); } void Inversion::contents_version_harvester(text_stream *text, text_file_position *tfp, void *state) { project *P = (project *) state; match_results mr = Regexp::create_mr(); if (Str::len(text) == 0) return; if (Regexp::match(&mr, text, L" *Version Number: *(%c*?) *")) { <Ensure a current version exists 3.6>; P->current_version->number = Str::duplicate(mr.exp[0]); } Regexp::dispose_of(&mr); }
The function Inversion::contents_harvester is used in §2.3.
The function Inversion::contents_version_harvester is used in §2.4.
§3.2. A version file contains lines which can either be a special command, or
give details of a version. The commands are Automatic
or Manual
(the
latter is the default), or Sync to W
, where W
is another project.
(All of this is infrastructure left over from when the Inform tools were
syncing version numbers to the main Inform 7 version number: with the
transition to Github, this scheme was dropped.)
void Inversion::version_harvester(text_stream *text, text_file_position *tfp, void *state) { project *P = (project *) state; match_results mr = Regexp::create_mr(); if (Str::len(text) == 0) return; if (Regexp::match(&mr, text, L"Automatic")) { P->manual_updating = FALSE; } else if (Regexp::match(&mr, text, L"Manual")) { P->manual_updating = TRUE; } else if (Regexp::match(&mr, text, L"Sync to (%c*)")) { P->sync_to = Inversion::read(mr.exp[0], TRUE); P->manual_updating = FALSE; } else if (Regexp::match(&mr, text, L"(%c*?)\t+(%c*?)\t+(%c*?)\t+(%c*?)\t+(%c*)")) { version *V = CREATE(version); V->name = Str::duplicate(mr.exp[0]); V->number = Str::duplicate(mr.exp[1]); V->build_code = Str::duplicate(mr.exp[2]); V->date = Str::duplicate(mr.exp[3]); V->notes = Str::duplicate(mr.exp[4]); if (Str::get_first_char(V->build_code) == '*') { Str::delete_first_character(V->build_code); P->current_version = V; } ADD_TO_LINKED_LIST(V, version, P->versions); } else { Errors::in_text_file("can't parse version line", tfp); } Regexp::dispose_of(&mr); }
The function Inversion::version_harvester is used in §2.5.
§3.3. Explicit code to read from header.h
in the Inform 6 repository.
void Inversion::header_harvester(text_stream *text, text_file_position *tfp, void *state) { project *P = (project *) state; match_results mr = Regexp::create_mr(); if (Str::len(text) == 0) return; if (Regexp::match(&mr, text, L"#define RELEASE_NUMBER (%c*?) *")) { <Ensure a current version exists 3.6>; P->current_version->number = Str::duplicate(mr.exp[0]); } if (Regexp::match(&mr, text, L"#define RELEASE_DATE \"(%c*?)\" *")) { <Ensure a current version exists 3.6>; P->current_version->name = Str::duplicate(mr.exp[0]); P->current_version->date = Str::duplicate(mr.exp[0]); } Regexp::dispose_of(&mr); }
The function Inversion::header_harvester is used in §2.6.
§3.4. Explicit code to read from the manifest file of a website template.
void Inversion::template_harvester(text_stream *text, text_file_position *tfp, void *state) { project *P = (project *) state; match_results mr = Regexp::create_mr(); if (Str::len(text) == 0) return; if (Regexp::match(&mr, text, L"%[INTERPRETERVERSION%]")) { P->next_is_version = TRUE; } else if (P->next_is_version) { <Ensure a current version exists 3.6>; P->current_version->name = Str::duplicate(text); P->next_is_version = FALSE; } Regexp::dispose_of(&mr); }
The function Inversion::template_harvester is used in §2.7.
§3.5. And this is needed for cheapglk and glulxe.
void Inversion::readme_harvester(text_stream *text, text_file_position *tfp, void *state) { project *P = (project *) state; match_results mr = Regexp::create_mr(); if (Str::len(text) == 0) return; if ((Regexp::match(&mr, text, L"CheapGlk Library: version (%c*?) *")) || (Regexp::match(&mr, text, L"- Version (%c*?) *"))) { <Ensure a current version exists 3.6>; P->current_version->number = Str::duplicate(mr.exp[0]); } Regexp::dispose_of(&mr); }
The function Inversion::readme_harvester is used in §2.8.
§3.6. And many of the above use this, which assumes there will be just one single version number known for a program.
<Ensure a current version exists 3.6> =
if (P->current_version == NULL) { version *V = CREATE(version); V->name = NULL; V->number = NULL; V->build_code = I"9Z99"; V->date = NULL; V->notes = NULL; ADD_TO_LINKED_LIST(V, version, P->versions); P->current_version = V; }
This code is used in §3, §3.1, §3.3 (twice), §3.4, §3.5.
§4. The following then writes back the versions file, following a version increment:
void Inversion::write(project *P) { text_stream vr_stream; text_stream *OUT = &vr_stream; if (Streams::open_to_file(OUT, P->versions_file, UTF8_ENC) == FALSE) Errors::fatal_with_file("can't write versions file", P->versions_file); if (P->sync_to) WRITE("Sync to %S\n", P->sync_to->web); else if (P->manual_updating) WRITE("Manual\n"); else WRITE("Automatic\n"); version *V; LOOP_OVER_LINKED_LIST(V, version, P->versions) { WRITE("%S\t%S\t", V->name, V->number); if (V == P->current_version) WRITE("*"); WRITE("%S\t%S\t%S\n", V->build_code, V->date, V->notes); } Streams::close(OUT); }
The function Inversion::write is used in §8.
§5. Updating. The standard date format we use is "26 February 2018".
int Inversion::dated_today(project *P, text_stream *dateline) { if (P->current_version == NULL) return FALSE; char *monthname[12] = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; WRITE_TO(dateline, "%d %s %d", the_present->tm_mday, monthname[the_present->tm_mon], the_present->tm_year+1900); int rv = FALSE; if (Str::eq(dateline, P->current_version->date)) rv = TRUE; return rv; }
The function Inversion::dated_today is used in §9.
§6. Here we read the Inform four-character code, e.g., 3Q27
, and increase it
by one. The two-digit code at the back is incremented, but rolls around from
99
to 01
, in which case the letter is advanced, except that I
and O
are skipped, and if the letter passes Z
then it rolls back around to A
and the initial digit is incremented.
void Inversion::increment(project *P) { if (P->current_version == NULL) return; text_stream *T = P->current_version->build_code; if (Str::len(T) != 4) Errors::with_text("version number malformed: %S", T); else { int N = Str::get_at(T, 0) - '0'; int L = Str::get_at(T, 1); int M1 = Str::get_at(T, 2) - '0'; int M2 = Str::get_at(T, 3) - '0'; if ((N < 0) || (N > 9) || (L < 'A') || (L > 'Z') || (M1 < 0) || (M1 > 9) || (M2 < 0) || (M2 > 9)) { Errors::with_text("version number malformed: %S", T); } else { M2++; if (M2 == 10) { M2 = 0; M1++; } if (M1 == 10) { M1 = 0; M2 = 1; L++; } if ((L == 'I') || (L == 'O')) L++; if (L > 'Z') { L = 'A'; N++; } if (N == 10) Errors::with_text("version number overflowed: %S", T); else { Str::clear(T); WRITE_TO(T, "%d%c%d%d", N, L, M1, M2); PRINT("Build advanced to %S\n", T); } } } }
The function Inversion::increment is used in §9.
§7. Imposition. When we impose a new version number on a web that has a contents page, we update the metadata in that contents page.
void Inversion::impose(project *P) { TextFiles::read(P->contents_file, FALSE, "unable to read web contents", TRUE, &Inversion::impose_helper, NULL, P); text_stream vr_stream; text_stream *OUT = &vr_stream; if (Streams::open_to_file(OUT, P->contents_file, UTF8_ENC) == FALSE) Errors::fatal_with_file("unable to write web contents", P->contents_file); WRITE("%S", P->conts); Streams::close(OUT); } void Inversion::impose_helper(text_stream *text, text_file_position *tfp, void *state) { project *P = (project *) state; project *SP = P; if (P->sync_to) SP = P->sync_to; text_stream *OUT = P->conts; match_results mr = Regexp::create_mr(); if ((P->current_version) && (Regexp::match(&mr, text, L"Build Date:%c*"))) { WRITE("Build Date: %S\n", SP->current_version->date); } else if ((P->current_version) && (Regexp::match(&mr, text, L"Build Number:%c*"))) { WRITE("Build Number: %S\n", SP->current_version->build_code); } else if ((P->current_version) && (Regexp::match(&mr, text, L"Version Number:%c*"))) { WRITE("Version Number: %S\n", P->current_version->number); } else if ((P->current_version) && (Regexp::match(&mr, text, L"Version Name:%c*"))) { WRITE("Version Name: %S\n", P->current_version->name); } else { WRITE("%S\n", text); } Regexp::dispose_of(&mr); }
The function Inversion::impose is used in §8.
The function Inversion::impose_helper appears nowhere else.
void Inversion::maintain(text_stream *web) { project *P = Inversion::read(web, FALSE); if (Inversion::needs_update(P)) { Inversion::write(P); Inversion::impose(P); } }
The function Inversion::maintain is used in 1/mn (§2).
int Inversion::needs_update(project *P) { int rv = FALSE; if ((P->manual_updating == FALSE) && (P->current_version)) { project *SP = P->sync_to; if (SP) { if (SP->current_version) { if (Str::ne(P->current_version->date, SP->current_version->date)) { rv = TRUE; Str::copy(P->current_version->date, SP->current_version->date); } if (Str::ne(P->current_version->build_code, SP->current_version->build_code)) { rv = TRUE; Str::copy(P->current_version->build_code, SP->current_version->build_code); } } } else { TEMPORARY_TEXT(dateline); if (Inversion::dated_today(P, dateline) == FALSE) { if (P->sync_to == NULL) Inversion::increment(P); Str::copy(P->current_version->date, dateline); rv = TRUE; } DISCARD_TEXT(dateline); } } return rv; }
The function Inversion::needs_update is used in §8.