1+ #!/usr/bin/env python3
2+
3+ """ generate_unsupported_in_drivermode.py
4+
5+ usage: python generate_unsupported_in_drivermode.py <path>/Options.td [<path>/llvm-tblgen]
6+
7+ This script generates a Lit regression test file that validates that options
8+ are only exposed to intended driver modes.
9+
10+ The options and driver modes are parsed from Options.td, whose path should be
11+ provided on the command line. See clang/include/clang/Driver/Options.td
12+
13+ The path to the TableGen executable can optionally be provided. Otherwise, the
14+ script will search for it.
15+
16+ Logic:
17+ 1) For each option, (records of class "Option"), and for each driver, (records of class "OptionVisibility")
18+ a. if the option's "Visibility" field includes the driver flavour, skip processing this option for this driver
19+ b. if the option is part of an option group, (the record has the "Group" property),
20+ and the group's "Visibility" field includes the driver flavor, skip processing this option for this driver
21+ c. otherwise this option is not supported by this driver flavor, and this pairing is saved for testing
22+ 2) For each unsupported pairing, generate a Lit RUN line, and a CHECK line to parse for expected output. Ex: "error: unknown argument"
23+ """
24+
25+ import sys
26+ import shutil
27+ import os
28+ import json
29+ import subprocess
30+ from pathlib import Path
31+
32+ LLVM_TABLEGEN = "llvm-tblgen"
33+ LIT_TEST_PATH = "../test/Driver/Inputs/unsupported-driver-options-check.ll"
34+ INCLUDE_PATH = "../../llvm/include"
35+ PREFIX = "CHECK-"
36+
37+ # Strings used in Options.td for various driver flavours
38+ OPTION_CC1AS = "CC1AsOption"
39+ OPTION_CC1 = "CC1Option"
40+ OPTION_CL = "CLOption"
41+ OPTION_DXC = "DXCOption"
42+ OPTION_DEFAULT = "DefaultVis"
43+ OPTION_FC1 = "FC1Option"
44+ OPTION_FLANG = "FlangOption"
45+
46+ # Error messages output from each driver
47+ ERROR_MSG_CC1AS = ": error: unknown argument"
48+ ERROR_MSG_CC1 = "error: unknown argument"
49+ ERROR_MSG_CL = "" # TODO
50+ ERROR_MSG_DXC = "" # TODO
51+ ERROR_MSG_DEFAULT = "clang: error: unknown argument"
52+ ERROR_MSG_FC1 = "error: unknown argument"
53+ ERROR_MSG_FLANG = "flang: error: unknown argument"
54+
55+ # Lit CHECK prefixes
56+ CHECK_PREFIX_CC1AS = PREFIX + OPTION_CC1AS
57+ CHECK_PREFIX_CC1 = PREFIX + OPTION_CC1
58+ CHECK_PREFIX_CL = PREFIX + OPTION_CL
59+ CHECK_PREFIX_DXC = PREFIX + OPTION_DXC
60+ CHECK_PREFIX_DEFAULT = PREFIX + OPTION_DEFAULT
61+ CHECK_PREFIX_FC1 = PREFIX + OPTION_FC1
62+ CHECK_PREFIX_FLANG = PREFIX + OPTION_FLANG
63+
64+ LIT_TEST_NOTE = ("; NOTE: This lit test was automatically generated to validate " +
65+ "unintentionally exposed arguments to various driver flavours.\n "
66+ "; NOTE: To make changes, see " + Path (__file__ ).resolve ().as_posix ()
67+ + " from which it was generated.\n \n " )
68+
69+ def print_usage ():
70+ """ Print valid usage of this script
71+ """
72+ sys .exit ( "usage: python " + sys .argv [0 ] + " <path>/Options.td [<path>/llvm-tblgen]" )
73+
74+ def find_file (file_name , search_path ):
75+ """ Find the given file name under a search path
76+ """
77+ result = []
78+
79+ for root , dir , files in os .walk (search_path ):
80+ if file_name in files :
81+ result .append (os .path .join (root , file_name ))
82+ return result
83+
84+ def is_valid_file (path , expected_name ):
85+ """ Is a file valid
86+ Check if a given path is to a file, and if it matches the expected file name
87+ """
88+ if path .is_file () and path .name == expected_name :
89+ return True
90+ else :
91+ return False
92+
93+ def find_tablegen ():
94+ """ Validate the TableGen executable
95+ """
96+ result = shutil .which (LLVM_TABLEGEN )
97+ if result is None :
98+ sys .exit ("Unable to find " + LLVM_TABLEGEN + ".\n Exiting" )
99+ else :
100+ print ("TableGen found: " + result )
101+ return result
102+
103+ def find_groups (group_sequence , options_json , option ):
104+ """ Find the groups for a given option
105+ Note that groups can themselves be part of groups, hence the recursion
106+ """
107+ group_json = options_json [option ]["Group" ]
108+
109+ if group_json is None :
110+ return
111+
112+ # Prevent circular group membership lookup
113+ for group in group_sequence :
114+ if group_json ["def" ] == group :
115+ return
116+
117+ group_sequence .append (group_json ["def" ])
118+ return find_groups (group_sequence , options_json , option )
119+
120+
121+ class UnsupportedDriverOption ():
122+ def __init__ (self , driver , option ):
123+ self .driver = driver
124+ self .option = option
125+
126+ # Validate the number of arguments have been passed
127+ argc = len (sys .argv )
128+ if argc < 2 or argc > 3 :
129+ print_usage ()
130+
131+ options_input_path = Path (sys .argv [1 ])
132+ tablegen_input_path = ""
133+ tablegen = None
134+ options_td = ""
135+ driver_sequence = []
136+ options_sequence = []
137+ unsupported_sequence = []
138+
139+ current_path = os .path .dirname (__file__ )
140+
141+ # Validate Options.td
142+ if not is_valid_file (options_input_path , "Options.td" ):
143+ print ("Invalid Options.td path. Searching for valid path..." )
144+
145+ relative_path = "../"
146+ search_path = os .path .join (current_path , relative_path )
147+
148+ file_search_list = find_file ("Options.td" , search_path )
149+ if len (file_search_list ) != 1 :
150+ print_usage ()
151+ sys .exit ("Unable to find Options.td.\n Exiting" )
152+ else :
153+ options_td = file_search_list [0 ]
154+ print (options_td )
155+ else :
156+ options_td = options_input_path .resolve ().as_posix ()
157+
158+ # Validate TableGen executable
159+ if argc > 2 :
160+ tablegen_input_path = Path (sys .argv [2 ])
161+ if not is_valid_file (tablegen_input_path , "llvm-tblgen" ):
162+ print ("Invalid tablegen path. Searching for valid path..." )
163+ tablegen = find_tablegen ()
164+ else :
165+ tablegen = tablegen_input_path .resolve ().as_posix ()
166+ else :
167+ tablegen = find_tablegen ()
168+
169+ # Run TableGen to convert Options.td to json
170+ options_json_str = subprocess .run ([ tablegen , "-I" , os .path .join (current_path , INCLUDE_PATH ), options_td , "-dump-json" ], stdout = subprocess .PIPE )
171+ options_json = json .loads (options_json_str .stdout .decode ('utf-8' ))
172+
173+ # Gather list of driver flavours
174+ for i in options_json ["!instanceof" ]["OptionVisibility" ]:
175+ driver_sequence .append (i )
176+
177+ # Gather list of options
178+ for i in options_json ["!instanceof" ]["Option" ]:
179+ options_sequence .append (i )
180+
181+ # Walk through the options list and find which drivers shouldn't be visible to each option
182+ for option in options_sequence :
183+ tmp_vis_list = []
184+ group_sequence = []
185+
186+ # Check for the option's explicit visibility
187+ for visibility in options_json [option ]["Visibility" ]:
188+ tmp_vis_list .append (visibility ["def" ])
189+
190+ # Check for the option's group's visibility
191+ find_groups (group_sequence , options_json , option )
192+ if len (group_sequence ) > 0 :
193+ for group_name in group_sequence :
194+ for visibility in options_json [group_name ]["Visibility" ]:
195+ tmp_vis_list .append (visibility ["def" ])
196+
197+ # Append to the unsupported list
198+ for driver in driver_sequence :
199+ if driver not in tmp_vis_list :
200+ unsupported_sequence .append (UnsupportedDriverOption (driver , option ))
201+
202+ # Generate the lit test
203+ try :
204+ with open (LIT_TEST_PATH , "w" ) as lit_file :
205+ try :
206+ lit_file .write (LIT_TEST_NOTE )
207+
208+ for i in unsupported_sequence :
209+ if i .driver == OPTION_CC1AS :
210+ lit_file .write (
211+ "; RUN: not clang -cc1as -" + i .option + " -help 2>&1 | FileCheck -check-prefix=" + CHECK_PREFIX_CC1AS + " %s\n " )
212+ continue
213+ if i .driver == OPTION_CC1 :
214+ lit_file .write (
215+ "; RUN: not clang -cc1 -" + i .option + " -help 2>&1 | FileCheck -check-prefix=" + CHECK_PREFIX_CC1 + " %s\n " )
216+ continue
217+ # if i.driver == OPTION_CL:
218+ # lit_file.write(
219+ # "; RUN: not clang-cl -" + i.option + " -help 2>&1 | FileCheck -check-prefix=" + CHECK_PREFIX_CL + " %s\n")
220+ # continue
221+ # if i.driver == OPTION_DXC:
222+ # lit_file.write(
223+ # "; RUN: not clang-dxc -" + i.option + " -help 2>&1 | FileCheck -check-prefix=" + CHECK_PREFIX_DXC + " %s\n")
224+ # continue
225+ if i .driver == OPTION_DEFAULT :
226+ lit_file .write (
227+ "; RUN: not clang -" + i .option + " -help 2>&1 | FileCheck -check-prefix=" + CHECK_PREFIX_DEFAULT + " %s\n " )
228+ continue
229+ if i .driver == OPTION_FC1 :
230+ lit_file .write (
231+ "; RUN: not flang -fc1 -" + i .option + " -help 2>&1 | FileCheck -check-prefix=" + CHECK_PREFIX_FC1 + " %s\n " )
232+ continue
233+ if i .driver == OPTION_FLANG :
234+ lit_file .write (
235+ "; RUN: not flang -" + i .option + " -help 2>&1 | FileCheck -check-prefix=" + CHECK_PREFIX_FLANG + " %s\n " )
236+
237+ lit_file .write ("; " + CHECK_PREFIX_CC1AS + ": " + ERROR_MSG_CC1AS + "\n " )
238+ lit_file .write ("; " + CHECK_PREFIX_CC1 + ": " + ERROR_MSG_CC1 + "\n " )
239+ lit_file .write ("; " + CHECK_PREFIX_CL + ": " + ERROR_MSG_CL + "\n " )
240+ lit_file .write ("; " + CHECK_PREFIX_DXC + ": " + ERROR_MSG_DXC + "\n " )
241+ lit_file .write ("; " + CHECK_PREFIX_DEFAULT + ": " + ERROR_MSG_DEFAULT + "\n " )
242+ lit_file .write ("; " + CHECK_PREFIX_FC1 + ": " + ERROR_MSG_FC1 + "\n " )
243+ lit_file .write ("; " + CHECK_PREFIX_FLANG + ": " + ERROR_MSG_FLANG + "\n " )
244+ except (IOError , OSError ):
245+ sys .exit ("Error writing to " + "LIT_TEST_PATH. Exiting" )
246+ except (FileNotFoundError , PermissionError , OSError ):
247+ sys .exit ("Error opening " + "LIT_TEST_PATH" + ". Exiting" )
248+ else :
249+ lit_file .close ()
0 commit comments