program ISCC; {$APPTYPE CONSOLE} { Inno Setup Copyright (C) 1997-2025 Jordan Russell Portions by Martijn Laan For conditions of distribution and use, see LICENSE.TXT. Command-line compiler } {x$DEFINE STATICCOMPILER} { For debugging purposes, remove the 'x' to have it link the compiler code into this program and not depend on ISCmplr.dll. You will also need to add the ..\Components and Src folders to the Delphi Compiler Search path in the project options. Also see IDE.MainForm's STATICCOMPILER and Compiler.Compile's STATICPREPROC. } uses SafeDLLPath in '..\Components\SafeDLLPath.pas', Windows, SysUtils, Classes, {$IFDEF STATICCOMPILER} Compiler.Compile, {$ENDIF} PathFunc in '..\Components\PathFunc.pas', TrustFunc in '..\Components\TrustFunc.pas', Shared.CommonFunc in 'Src\Shared.CommonFunc.pas', Shared.CompilerInt in 'Src\Shared.CompilerInt.pas', Shared.CompilerInt.Struct in 'Src\Shared.CompilerInt.Struct.pas', Shared.FileClass in 'Src\Shared.FileClass.pas', Shared.ConfigIniFile in 'Src\Shared.ConfigIniFile.pas', Shared.SignToolsFunc in 'Src\Shared.SignToolsFunc.pas', Shared.LicenseFunc in 'Src\Shared.LicenseFunc.pas', SHA256 in '..\Components\SHA256.pas', ECDSA in '..\Components\ECDSA.pas', ISSigFunc in '..\Components\ISSigFunc.pas', StringScanner in '..\Components\StringScanner.pas', UnsignedFunc in '..\Components\UnsignedFunc.pas'; {$SETPEOSVERSION 6.1} {$SETPESUBSYSVERSION 6.1} {$WEAKLINKRTTI ON} {$R Res\ISCC.manifest.res} {$R Res\ISCC.versionandicon.res} type PScriptLine = ^TScriptLine; TScriptLine = record LineText: String; Next: PScriptLine; end; TOptionID = 0..25; TOptions = set of TOptionID; TIsppOptions = record ParserOptions: TOptions; Options: TOptions; VerboseLevel: Integer; InlineStart: String; InlineEnd: String; end; var StdOutHandle, StdErrHandle: THandle; StdOutHandleIsConsole, StdErrHandleIsConsole: Boolean; ScriptFilename: String; Definitions, IncludePath, IncludeFiles, Output, OutputPath, OutputFilename: String; SignTools: TStringList; ScriptLines, NextScriptLine: PScriptLine; CurLine: String; StartTime, EndTime: DWORD; Quiet, ShowProgress, WantAbort: Boolean; ProgressPoint: TPoint; LastProgress: String; IsppOptions: TIsppOptions; IsppMode: Boolean; procedure WriteToStdHandle(const Handle: THandle; const HandleIsConsole: Boolean; S: String); begin if Copy(S, 1, 1) <> #13 then S := S + #13#10; if HandleIsConsole then begin var CharsWritten: DWORD; WriteConsole(Handle, @S[1], ULength(S), CharsWritten, nil); end else begin var Utf8S := Utf8Encode(S); var BytesWritten: DWORD; WriteFile(Handle, Utf8S[1], ULength(Utf8S), BytesWritten, nil); end; end; procedure WriteStdOut(const S: String; const Warning: Boolean = False); var CSBI: TConsoleScreenBufferInfo; DidSetColor: Boolean; begin DidSetColor := Warning and StdOutHandleIsConsole and GetConsoleScreenBufferInfo(StdOutHandle, CSBI) and SetConsoleTextAttribute(StdOutHandle, FOREGROUND_INTENSITY or FOREGROUND_RED or FOREGROUND_GREEN); try WriteToStdHandle(StdOutHandle, StdOutHandleIsConsole, S); finally if DidSetColor then SetConsoleTextAttribute(StdOutHandle, CSBI.wAttributes); end; end; procedure WriteStdErr(const S: String; const Error: Boolean = False); var CSBI: TConsoleScreenBufferInfo; DidSetColor: Boolean; begin DidSetColor := Error and StdErrHandleIsConsole and GetConsoleScreenBufferInfo(StdErrHandle, CSBI) and SetConsoleTextAttribute(StdErrHandle, FOREGROUND_INTENSITY or FOREGROUND_RED); try WriteToStdHandle(StdErrHandle, StdErrHandleIsConsole, S); finally if DidSetColor then SetConsoleTextAttribute(StdErrHandle, CSBI.wAttributes); end; end; function GetCursorPos: TPoint; var CSBI: TConsoleScreenBufferInfo; begin if not StdOutHandleIsConsole or not GetConsoleScreenBufferInfo(StdOutHandle, CSBI) then Exit; Result.X := CSBI.dwCursorPosition.X; Result.Y := CSBI.dwCursorPosition.Y; end; procedure SetCursorPos(const P: TPoint); var Coords: TCoord; CSBI: TConsoleScreenBufferInfo; begin if not StdOutHandleIsConsole or not GetConsoleScreenBufferInfo(StdOutHandle, CSBI) then Exit; if P.X < 0 then Exit; if P.Y < 0 then Exit; if P.X > CSBI.dwSize.X then Exit; if P.Y > CSBI.dwSize.Y then Exit; Coords.X := SHORT(P.X); Coords.Y := SHORT(P.Y); SetConsoleCursorPosition(StdOutHandle, Coords); end; procedure WriteProgress(const S: String); var CSBI: TConsoleScreenBufferInfo; Str: String; begin if StdOutHandleIsConsole and GetConsoleScreenBufferInfo(StdOutHandle, CSBI) then begin if Length(S) > CSBI.dwSize.X then Str := Copy(S, 1, CSBI.dwSize.X) else Str := Format('%-' + IntToStr(CSBI.dwSize.X) + 's', [S]); end else Str := S; WriteToStdHandle(StdOutHandle, StdOutHandleIsConsole, Str); end; function ConsoleCtrlHandler(dwCtrlType: DWORD): BOOL; stdcall; begin { Abort gracefully when Ctrl+C/Break is pressed } WantAbort := True; Result := True; end; procedure ReadScriptLines(const F: TTextFileReader); var LineNumber: Integer; PrevLine, L: PScriptLine; begin LineNumber := 1; PrevLine := nil; while not F.Eof do begin New(L); try L.LineText := F.ReadLine; if Pos(#0, L.LineText) <> 0 then raise Exception.CreateFmt('Illegal null character on line %d', [LineNumber]); L.Next := nil; except Dispose(L); raise; end; if Assigned(PrevLine) then PrevLine.Next := L else begin ScriptLines := L; NextScriptLine := L; end; PrevLine := L; Inc(LineNumber); end; end; procedure FreeScriptLines; var L, NextLine: PScriptLine; begin L := ScriptLines; ScriptLines := nil; NextScriptLine := nil; while Assigned(L) do begin NextLine := L.Next; Dispose(L); L := NextLine; end; end; function CompilerCallbackProc(Code: Integer; var Data: TCompilerCallbackData; AppData: Longint): Integer; stdcall; procedure PrintProgress(Progress: String); var Pt: TPoint; begin if (Progress = '') or (LastProgress = Progress) then Exit; Pt := GetCursorPos; if Pt.Y <= ProgressPoint.Y then Exit else if ProgressPoint.X < 0 then begin ProgressPoint := Pt; WriteStdOut(''); Pt := GetCursorPos; end; SetCursorPos(ProgressPoint); WriteProgress(#13 + Progress); LastProgress := Progress; SetCursorPos(Pt); end; var S, BytesCompressedPerSecond, SecondsRemaining: String; begin if WantAbort then begin Result := iscrRequestAbort; Exit; end; Result := iscrSuccess; case Code of iscbReadScript: begin { Note: In Inno Setup 3.0.1 and later we can ignore Data.Reset since it is only True once (when reading the first line). } if Assigned(NextScriptLine) then begin CurLine := NextScriptLine.LineText; NextScriptLine := NextScriptLine.Next; Data.LineRead := PChar(CurLine); end; end; iscbNotifyStatus: if not Quiet then WriteStdOut(Data.StatusMsg, Data.Warning) else if ShowProgress then PrintProgress(Trim(Data.StatusMsg)); iscbNotifySuccess: begin EndTime := GetTickCount; if not Quiet then begin WriteStdOut(''); if Data.OutputExeFilename <> '' then begin WriteStdOut(Format('Successful compile (%.3f sec). ' + 'Resulting Setup program filename is:', [(EndTime - StartTime) / 1000])); WriteStdOut(Data.OutputExeFilename); end else WriteStdOut(Format('Successful compile (%.3f sec). ' + 'Output was disabled.', [(EndTime - StartTime) / 1000])); end; end; iscbNotifyError: if Assigned(Data.ErrorMsg) then begin S := 'Error'; if Data.ErrorLine <> 0 then S := S + Format(' on line %d', [Data.ErrorLine]); if Assigned(Data.ErrorFilename) then S := S + ' in ' + Data.ErrorFilename else if ScriptFilename <> '' then S := S + ' in ' + ScriptFilename; S := S + ': ' + Data.ErrorMsg; WriteStdErr(S, True); end; iscbNotifyIdle: if ShowProgress and (Data.CompressProgress <> 0) then begin if Data.BytesCompressedPerSecond <> 0 then BytesCompressedPerSecond := Format(' at %.2f kb/s', [Data.BytesCompressedPerSecond / 1024]) else BytesCompressedPerSecond := ''; if Data.SecondsRemaining <> -1 then SecondsRemaining := Format(', %d seconds remaining', [Data.SecondsRemaining]) else SecondsRemaining := ''; PrintProgress(Format('Compressing: %.2f%% done%s%s', [Data.CompressProgress / Data.CompressProgressMax * 100, BytesCompressedPerSecond, SecondsRemaining])); end; end; end; procedure ProcessCommandLine; procedure SetOption(var Options: TOptions; Option: Char; Value: Boolean); begin if Value then Include(Options, Ord(UpCase(Option)) - Ord('A')) else Exclude(Options, Ord(UpCase(Option)) - Ord('A')) end; procedure InitIsppOptions(var Opt: TIsppOptions; var Definitions, IncludePath, IncludeFiles: String); begin with Opt do begin SetOption(Options, 'C', True); SetOption(ParserOptions, 'B', True); SetOption(ParserOptions, 'P', True); VerboseLevel := 0; InlineStart := '{#'; InlineEnd := '}'; end; Definitions := 'ISCC_INVOKED'#1'ISPPCC_INVOKED'#1; IncludePath := PathExtractDir(NewParamStr(0)); IncludeFiles := ''; end; procedure ReadOptionsParam(var Options: TOptions; Symbol: Char); var I: Integer; S: String; begin for I := 1 to NewParamCount do begin S := NewParamStr(I); if Length(S) = 4 then if ((S[1] = '/') or (S[1] = '-')) and (UpCase(S[2]) = Symbol) then case S[4] of '-': SetOption(Options, S[3], False); '+': SetOption(Options, S[3], True) else raise Exception.CreateFmt('Invalid command line option: %s', [S]); end; end; end; function IsParam(const S: String): Boolean; begin Result := (Length(S) >= 2) and ((S[1] = '/') or (S[1] = '-')); end; function GetParam(var S: String; Symbols: String): Boolean; begin Result := IsParam(S) and (CompareText(Copy(S, 2, Length(Symbols)), Symbols) = 0); if Result then S := Copy(S, 2 + Length(Symbols), MaxInt); end; function FindParam(var Index: Integer; Symbols: String): String; var I: Integer; S: String; begin for I := Index to NewParamCount do begin S := NewParamStr(I); if IsParam(S) and (CompareText(Copy(S, 2, Length(Symbols)), Symbols) = 0) then begin Result := Copy(S, 2 + Length(Symbols), MaxInt); Index := I + 1; Exit; end; end; Index := MaxInt; Result := ''; end; procedure ShowBanner; begin WriteStdOut('Inno Setup 6 Command-Line Compiler'); WriteStdOut('Copyright (C) 1997-2026 Jordan Russell. All rights reserved.'); WriteStdOut('Portions Copyright (C) 2000-2026 Martijn Laan. All rights reserved.'); if IsppMode then WriteStdOut('Portions Copyright (C) 2001-2004 Alex Yackimoff. All rights reserved.'); WriteStdOut('https://www.innosetup.com'); WriteStdOut(''); end; procedure ShowUsage; begin WriteStdErr('Usage: iscc [options] scriptfile.iss'); WriteStdErr('or to read from standard input: iscc [options] -'); WriteStdErr('Options:'); WriteStdErr(' /O(+|-) Enable or disable output (overrides Output)'); WriteStdErr(' /O Output files to specified path (overrides OutputDir)'); WriteStdErr(' /F Specifies an output filename (overrides OutputBaseFilename)'); WriteStdErr(' /S= Sets a SignTool with the specified name and command'); WriteStdErr(' (Any Sign Tools configured using the Compiler IDE will be specified automatically)'); WriteStdErr(' /Q Quiet compile (print error messages only)'); WriteStdErr(' /Qp Enable quiet compile while still displaying progress'); if IsppMode then begin WriteStdErr(' /D[=] Emulate #define public '); WriteStdErr(' /$(+|-) Emulate #pragma option -(+|-)'); WriteStdErr(' /P(+|-) Emulate #pragma parseroption -(+|-)'); WriteStdErr(' /I Emulate #pragma include '); WriteStdErr(' /J Emulate #include '); WriteStdErr(' /{# Emulate #pragma inlinestart '); WriteStdErr(' /} Emulate #pragma inlineend '); WriteStdErr(' /V Emulate #pragma verboselevel '); end; WriteStdErr(' /? Show this help screen'); WriteStdErr(''); WriteStdErr('Examples: iscc "c:\isetup\samples\my script.iss"'); WriteStdErr(' iscc /Qp /O"My Output" /F"MyProgram-1.0" /Sbyparam=$p "c:\isetup\samples\my script.iss"'); if IsppMode then begin WriteStdErr(' iscc /$c- /Pu+ "/DLic=Trial Lic.txt" /IC:\INC;D:\INC scriptfile.iss'); WriteStdErr(''); end; end; var I: Integer; S: String; begin if IsppMode then begin InitIsppOptions(IsppOptions, Definitions, IncludePath, IncludeFiles); { Also see below } ReadOptionsParam(IsppOptions.Options, '$'); ReadOptionsParam(IsppOptions.ParserOptions, 'P'); end; for I := 1 to NewParamCount do begin S := NewParamStr(I); if (S = '') or IsParam(S) then begin if GetParam(S, 'Q') then begin Quiet := True; ShowProgress := CompareText(S, 'P') = 0; end else if GetParam(S, 'O') then begin if S = '-' then Output := 'no' else if S = '+' then Output := 'yes' else OutputPath := S; end else if GetParam(S, 'F') then OutputFilename := S else if GetParam(S, 'S') then begin if Pos('=', S) = 0 then begin ShowBanner; WriteStdErr('Invalid option: ' + S, True); Halt(1); end; SignTools.Add(S); end else if IsppMode and GetParam(S, 'D') then begin Definitions := Definitions + S + #1; end else if IsppMode and GetParam(S, 'I') then begin IncludePath := IncludePath + ';' + S; end else if IsppMode and GetParam(S, 'J') then begin IncludeFiles := IncludeFiles + S + #1; end else if IsppMode and GetParam(S, '{#') then begin if S <> '' then IsppOptions.InlineStart := S; end else if IsppMode and GetParam(S, '}') then begin if S <> '' then IsppOptions.InlineEnd := S; end else if IsppMode and GetParam(S, 'V') then begin if S <> '' then IsppOptions.VerboseLevel := StrToIntDef(S, 0); end else if IsppMode and (GetParam(S, '$') or GetParam(S, 'P')) then begin { Already handled above } end else if S = '/?' then begin ShowBanner; ShowUsage; Halt(1); end else begin ShowBanner; WriteStdErr('Unknown option: ' + S, True); Halt(1); end; end else begin { Not a switch; must be the script filename } if ScriptFilename <> '' then begin ShowBanner; WriteStdErr('You may not specify more than one script filename.', True); Halt(1); end; ScriptFilename := S; end; end; if ScriptFilename = '' then begin ShowBanner; ShowUsage; Halt(1); end; if not Quiet then ShowBanner; end; procedure Go; procedure AppendOption(var Opts: String; const OptName, OptValue: String); begin Opts := Opts + OptName + '=' + OptValue + #0; end; function ConvertOptionsToString(const Options: TOptions): String; var I: TOptionID; begin Result := ''; for I := 0 to 25 do if I in Options then Result := Result + Chr(Ord('a') + I); end; procedure IsppOptionsToString(var S: String; Opt: TIsppOptions; Definitions, IncludePath, IncludeFiles: String); begin with Opt do begin AppendOption(S, 'ISPP:ParserOptions', ConvertOptionsToString(ParserOptions)); AppendOption(S, 'ISPP:Options', ConvertOptionsToString(Options)); AppendOption(S, 'ISPP:VerboseLevel', IntToStr(VerboseLevel)); AppendOption(S, 'ISPP:InlineStart', InlineStart); AppendOption(S, 'ISPP:InlineEnd', InlineEnd); end; AppendOption(S, 'ISPP:Definitions', Definitions); AppendOption(S, 'ISPP:IncludePath', IncludePath); AppendOption(S, 'ISPP:IncludeFiles', IncludeFiles); end; var ScriptPath: String; ExitCode: Word; Ver: PCompilerVersionInfo; F: TTextFileReader; Params: TCompileScriptParamsEx; Options: String; Res: Integer; I: Integer; IDESignTools: TStringList; begin if ScriptFilename <> '-' then begin ScriptFilename := PathExpand(ScriptFilename); ScriptPath := PathExtractPath(ScriptFilename); end else begin { Read from standard input } ScriptFilename := ''; ScriptPath := GetCurrentDir; end; {$IFNDEF STATICCOMPILER} try InitISCmplrLibrary; except begin WriteStdErr(Format('Could not load %s: %s', [ISCmplrDLL, GetExceptMessage]), True); Halt(1); end; end; Ver := ISDllGetVersion; {$ELSE} Ver := ISGetVersion; {$ENDIF} if Ver.BinVersion < $05000500 then begin { 5.0.5 or later is required since we use TCompileScriptParamsEx } WriteStdErr('Incompatible compiler engine version.', True); Halt(1); end; ProgressPoint.X := -1; ExitCode := 0; try if ScriptFilename <> '' then F := TTextFileReader.Create(ScriptFilename, fdOpenExisting, faRead, fsRead) else F := TTextFileReader.CreateWithExistingHandle(GetStdHandle(STD_INPUT_HANDLE)); try ReadScriptLines(F); finally F.Free; end; if not Quiet then begin WriteStdOut('Compiler engine version: ' + String(Ver.Title) + ' ' + String(Ver.Version)); if IsLicensed then WriteStdOut('Licensee name: ' + GetLicenseeDescription) else WriteStdOut(GetLicenseeDescription); WriteStdOut(''); end; FillChar(Params, SizeOf(Params), 0); Params.Size := SizeOf(Params); Params.SourcePath := PChar(ScriptPath); Params.CallbackProc := CompilerCallbackProc; Options := ''; if Output <> '' then AppendOption(Options, 'Output', Output); if OutputPath <> '' then AppendOption(Options, 'OutputDir', OutputPath); if OutputFilename <> '' then AppendOption(Options, 'OutputBaseFilename', OutputFilename); for I := 0 to SignTools.Count-1 do Options := Options + AddSignToolParam(SignTools[I]); IDESignTools := TStringList.Create; try { Also automatically read and add SignTools defined using the IDE. Adding these after the command line SignTools so that the latter are always found first by the compiler. } ReadSignTools(IDESignTools); for I := 0 to IDESignTools.Count-1 do Options := Options + AddSignToolParam(IDESignTools[I]); finally IDESignTools.Free; end; if IsppMode then IsppOptionsToString(Options, IsppOptions, Definitions, IncludePath, IncludeFiles); Params.Options := PChar(Options); StartTime := GetTickCount; {$IFNDEF STATICCOMPILER} Res := ISDllCompileScript(Params); {$ELSE} Res := ISCompileScript(Params, False); {$ENDIF} case Res of isceNoError: ; isceCompileFailure: begin ExitCode := 2; WriteStdErr('Compile aborted.', True); end; else ExitCode := 1; WriteStdErr(Format('Internal error: ISDllCompileScript returned ' + 'unexpected result (%d).', [Res]), True); end; finally FreeScriptLines; end; if ExitCode <> 0 then Halt(ExitCode); end; function ISPPInstalled: Boolean; begin Result := NewFileExists(PathExtractPath(NewParamStr(0)) + 'ISPP.dll'); end; begin {$IFDEF DEBUG} ReportMemoryLeaksOnShutdown := True; {$ENDIF} SignTools := TStringList.Create; try StdOutHandle := GetStdHandle(STD_OUTPUT_HANDLE); StdErrHandle := GetStdHandle(STD_ERROR_HANDLE); var Mode: DWORD; StdOutHandleIsConsole := GetConsoleMode(StdOutHandle, Mode); StdErrHandleIsConsole := GetConsoleMode(StdErrHandle, Mode); SetConsoleCtrlHandler(@ConsoleCtrlHandler, True); try ReadLicense; IsppMode := ISPPInstalled; ProcessCommandLine; Go; except { Show a friendlier exception message. (By default, Delphi prints out the exception class and address.) } WriteStdErr(GetExceptMessage, True); Halt(2); end; finally SignTools.Free; end; end.