@@ -18,12 +18,14 @@ import * as fs from "fs/promises";
1818import * as fsSync from "fs" ;
1919import * as os from "os" ;
2020import * as readline from "readline" ;
21- import { execFile , ExecFileError } from "../utilities/utilities" ;
21+ import * as Stream from "stream" ;
22+ import { execFile , ExecFileError , execFileStreamOutput } from "../utilities/utilities" ;
2223import * as vscode from "vscode" ;
2324import { Version } from "../utilities/version" ;
2425import { z } from "zod/v4/mini" ;
2526import { SwiftLogger } from "../logging/SwiftLogger" ;
2627import { findBinaryPath } from "../utilities/shell" ;
28+ import { SwiftOutputChannel } from "../logging/SwiftOutputChannel" ;
2729
2830const ListResult = z . object ( {
2931 toolchains : z . array (
@@ -94,6 +96,12 @@ export interface SwiftlyProgressData {
9496 } ;
9597}
9698
99+ export interface PostInstallValidationResult {
100+ isValid : boolean ;
101+ summary : string ;
102+ invalidCommands ?: string [ ] ;
103+ }
104+
97105export class Swiftly {
98106 /**
99107 * Finds the version of Swiftly installed on the system.
@@ -326,13 +334,6 @@ export class Swiftly {
326334 throw new Error ( "Swiftly is not supported on this platform" ) ;
327335 }
328336
329- if ( process . platform === "linux" ) {
330- logger ?. info (
331- `Skipping toolchain installation on Linux as it requires PostInstall steps`
332- ) ;
333- return ;
334- }
335-
336337 logger ?. info ( `Installing toolchain ${ version } via swiftly` ) ;
337338
338339 const tmpDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "vscode-swift-" ) ) ;
@@ -392,6 +393,10 @@ export class Swiftly {
392393 } else {
393394 await installPromise ;
394395 }
396+
397+ if ( process . platform === "linux" ) {
398+ await this . handlePostInstallFile ( postInstallFilePath , version , logger ) ;
399+ }
395400 } finally {
396401 if ( progressPipePath ) {
397402 try {
@@ -400,6 +405,210 @@ export class Swiftly {
400405 // Ignore errors if the pipe file doesn't exist
401406 }
402407 }
408+ try {
409+ await fs . unlink ( postInstallFilePath ) ;
410+ } catch {
411+ // Ignore errors if the post-install file doesn't exist
412+ }
413+ }
414+ }
415+
416+ /**
417+ * Handles post-install file created by swiftly installation (Linux only)
418+ *
419+ * @param postInstallFilePath Path to the post-install script
420+ * @param version The toolchain version being installed
421+ * @param logger Optional logger for error reporting
422+ */
423+ private static async handlePostInstallFile (
424+ postInstallFilePath : string ,
425+ version : string ,
426+ logger ?: SwiftLogger
427+ ) : Promise < void > {
428+ try {
429+ await fs . access ( postInstallFilePath ) ;
430+ } catch {
431+ logger ?. info ( `No post-install steps required for toolchain ${ version } ` ) ;
432+ return ;
433+ }
434+
435+ logger ?. info ( `Post-install file found for toolchain ${ version } ` ) ;
436+
437+ const validation = await this . validatePostInstallScript ( postInstallFilePath , logger ) ;
438+
439+ if ( ! validation . isValid ) {
440+ const errorMessage = `Post-install script contains unsafe commands. Invalid commands: ${ validation . invalidCommands ?. join ( ", " ) } ` ;
441+ logger ?. error ( errorMessage ) ;
442+ void vscode . window . showErrorMessage (
443+ `Installation of Swift ${ version } requires additional system packages, but the post-install script contains commands that are not allowed for security reasons.`
444+ ) ;
445+ return ;
446+ }
447+
448+ const shouldExecute = await this . showPostInstallConfirmation ( version , validation ) ;
449+
450+ if ( shouldExecute ) {
451+ await this . executePostInstallScript ( postInstallFilePath , version , logger ) ;
452+ } else {
453+ void vscode . window . showWarningMessage (
454+ `Swift ${ version } installation is incomplete. You may need to manually install additional system packages.`
455+ ) ;
456+ }
457+ }
458+
459+ /**
460+ * Validates post-install script commands against allow-list patterns.
461+ * Supports apt-get and yum package managers only.
462+ *
463+ * @param postInstallFilePath Path to the post-install script
464+ * @param logger Optional logger for error reporting
465+ * @returns Validation result with command summary
466+ */
467+ private static async validatePostInstallScript (
468+ postInstallFilePath : string ,
469+ logger ?: SwiftLogger
470+ ) : Promise < PostInstallValidationResult > {
471+ try {
472+ const scriptContent = await fs . readFile ( postInstallFilePath , "utf-8" ) ;
473+ const lines = scriptContent
474+ . split ( "\n" )
475+ . filter ( line => line . trim ( ) && ! line . trim ( ) . startsWith ( "#" ) ) ;
476+
477+ const allowedPatterns = [
478+ / ^ a p t - g e t \s + - y \s + i n s t a l l ( \s + [ A - Z a - z 0 - 9 \- _ . + ] + ) + \s * $ / , // apt-get -y install packages
479+ / ^ y u m \s + i n s t a l l ( \s + [ A - Z a - z 0 - 9 \- _ . + ] + ) + \s * $ / , // yum install packages
480+ / ^ \s * $ | ^ # .* $ / , // empty lines and comments
481+ ] ;
482+
483+ const invalidCommands : string [ ] = [ ] ;
484+ const packageInstallCommands : string [ ] = [ ] ;
485+
486+ for ( const line of lines ) {
487+ const trimmedLine = line . trim ( ) ;
488+ if ( ! trimmedLine ) {
489+ continue ;
490+ }
491+
492+ const isValid = allowedPatterns . some ( pattern => pattern . test ( trimmedLine ) ) ;
493+
494+ if ( ! isValid ) {
495+ invalidCommands . push ( trimmedLine ) ;
496+ } else if ( trimmedLine . includes ( "install" ) ) {
497+ packageInstallCommands . push ( trimmedLine ) ;
498+ }
499+ }
500+
501+ const isValid = invalidCommands . length === 0 ;
502+
503+ let summary = "The script will perform the following actions:\n" ;
504+ if ( packageInstallCommands . length > 0 ) {
505+ summary += `• Install system packages using package manager\n` ;
506+ summary += `• Commands: ${ packageInstallCommands . join ( "; " ) } ` ;
507+ } else {
508+ summary += "• No package installations detected" ;
509+ }
510+
511+ return {
512+ isValid,
513+ summary,
514+ invalidCommands : invalidCommands . length > 0 ? invalidCommands : undefined ,
515+ } ;
516+ } catch ( error ) {
517+ logger ?. error ( `Failed to validate post-install script: ${ error } ` ) ;
518+ return {
519+ isValid : false ,
520+ summary : "Failed to read post-install script" ,
521+ invalidCommands : [ "Unable to read script file" ] ,
522+ } ;
523+ }
524+ }
525+
526+ /**
527+ * Shows confirmation dialog to user for executing post-install script
528+ *
529+ * @param version The toolchain version being installed
530+ * @param validation The validation result
531+ * @returns Promise resolving to user's decision
532+ */
533+ private static async showPostInstallConfirmation (
534+ version : string ,
535+ validation : PostInstallValidationResult
536+ ) : Promise < boolean > {
537+ const message =
538+ `Swift ${ version } installation requires additional system packages to be installed. ` +
539+ `This will require administrator privileges.\n\n${ validation . summary } \n\n` +
540+ `Do you want to proceed with running the post-install script?` ;
541+
542+ const choice = await vscode . window . showWarningMessage (
543+ message ,
544+ { modal : true } ,
545+ "Execute Script" ,
546+ "Cancel"
547+ ) ;
548+
549+ return choice === "Execute Script" ;
550+ }
551+
552+ /**
553+ * Executes post-install script with elevated permissions (Linux only)
554+ *
555+ * @param postInstallFilePath Path to the post-install script
556+ * @param version The toolchain version being installed
557+ * @param logger Optional logger for error reporting
558+ */
559+ private static async executePostInstallScript (
560+ postInstallFilePath : string ,
561+ version : string ,
562+ logger ?: SwiftLogger
563+ ) : Promise < void > {
564+ logger ?. info ( `Executing post-install script for toolchain ${ version } ` ) ;
565+
566+ const outputChannel = new SwiftOutputChannel (
567+ `Swift ${ version } Post-Install` ,
568+ path . join ( os . tmpdir ( ) , `swift-post-install-${ version } .log` )
569+ ) ;
570+
571+ try {
572+ outputChannel . show ( true ) ;
573+ outputChannel . appendLine ( `Executing post-install script for Swift ${ version } ...` ) ;
574+ outputChannel . appendLine ( `Script location: ${ postInstallFilePath } ` ) ;
575+ outputChannel . appendLine ( "" ) ;
576+
577+ await execFile ( "chmod" , [ "+x" , postInstallFilePath ] ) ;
578+
579+ const command = "pkexec" ;
580+ const args = [ postInstallFilePath ] ;
581+
582+ outputChannel . appendLine ( `Executing: ${ command } ${ args . join ( " " ) } ` ) ;
583+ outputChannel . appendLine ( "" ) ;
584+
585+ const outputStream = new Stream . Writable ( {
586+ write ( chunk , _encoding , callback ) {
587+ const text = chunk . toString ( ) ;
588+ outputChannel . append ( text ) ;
589+ callback ( ) ;
590+ } ,
591+ } ) ;
592+
593+ await execFileStreamOutput ( command , args , outputStream , outputStream , null , { } ) ;
594+
595+ outputChannel . appendLine ( "" ) ;
596+ outputChannel . appendLine (
597+ `Post-install script completed successfully for Swift ${ version } `
598+ ) ;
599+
600+ void vscode . window . showInformationMessage (
601+ `Swift ${ version } post-install script executed successfully. Additional system packages have been installed.`
602+ ) ;
603+ } catch ( error ) {
604+ const errorMsg = `Failed to execute post-install script: ${ error } ` ;
605+ logger ?. error ( errorMsg ) ;
606+ outputChannel . appendLine ( "" ) ;
607+ outputChannel . appendLine ( `Error: ${ errorMsg } ` ) ;
608+
609+ void vscode . window . showErrorMessage (
610+ `Failed to execute post-install script for Swift ${ version } . Check the output channel for details.`
611+ ) ;
403612 }
404613 }
405614
0 commit comments