configuration.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. #!/usr/bin/env python3
  2. #
  3. # configuration.py
  4. # Apply options from config.ini to the existing Configuration headers
  5. #
  6. import re, shutil, configparser, datetime
  7. from pathlib import Path
  8. verbose = 0
  9. def blab(str,level=1):
  10. if verbose >= level: print(f"[config] {str}")
  11. def config_path(cpath):
  12. return Path("Marlin", cpath)
  13. # Apply a single name = on/off ; name = value ; etc.
  14. # TODO: Limit to the given (optional) configuration
  15. def apply_opt(name, val, conf=None):
  16. if name == "lcd": name, val = val, "on"
  17. # Create a regex to match the option and capture parts of the line
  18. # 1: Indentation
  19. # 2: Comment
  20. # 3: #define and whitespace
  21. # 4: Option name
  22. # 5: First space after name
  23. # 6: Remaining spaces between name and value
  24. # 7: Option value
  25. # 8: Whitespace after value
  26. # 9: End comment
  27. regex = re.compile(rf'^(\s*)(//\s*)?(#define\s+)({name}\b)(\s?)(\s*)(.*?)(\s*)(//.*)?$', re.IGNORECASE)
  28. # Find and enable and/or update all matches
  29. for file in ("Configuration.h", "Configuration_adv.h"):
  30. fullpath = config_path(file)
  31. lines = fullpath.read_text(encoding='utf-8').split('\n')
  32. found = False
  33. for i in range(len(lines)):
  34. line = lines[i]
  35. match = regex.match(line)
  36. if match and match[4].upper() == name.upper():
  37. found = True
  38. # For boolean options un/comment the define
  39. if val in ("on", "", None):
  40. newline = re.sub(r'^(\s*)//+\s*(#define)(\s{1,3})?(\s*)', r'\1\2 \4', line)
  41. elif val == "off":
  42. # TODO: Comment more lines in a multi-line define with \ continuation
  43. newline = re.sub(r'^(\s*)(#define)(\s{1,3})?(\s*)', r'\1//\2 \4', line)
  44. else:
  45. # For options with values, enable and set the value
  46. addsp = '' if match[5] else ' '
  47. newline = match[1] + match[3] + match[4] + match[5] + addsp + val + match[6]
  48. if match[9]:
  49. sp = match[8] if match[8] else ' '
  50. newline += sp + match[9]
  51. lines[i] = newline
  52. blab(f"Set {name} to {val}")
  53. # If the option was found, write the modified lines
  54. if found:
  55. fullpath.write_text('\n'.join(lines), encoding='utf-8')
  56. break
  57. # If the option didn't appear in either config file, add it
  58. if not found:
  59. # OFF options are added as disabled items so they appear
  60. # in config dumps. Useful for custom settings.
  61. prefix = ""
  62. if val == "off":
  63. prefix, val = "//", "" # Item doesn't appear in config dump
  64. #val = "false" # Item appears in config dump
  65. # Uppercase the option unless already mixed/uppercase
  66. added = name.upper() if name.islower() else name
  67. # Add the provided value after the name
  68. if val != "on" and val != "" and val is not None:
  69. added += " " + val
  70. # Prepend the new option after the first set of #define lines
  71. fullpath = config_path("Configuration.h")
  72. with fullpath.open(encoding='utf-8') as f:
  73. lines = f.readlines()
  74. linenum = 0
  75. gotdef = False
  76. for line in lines:
  77. isdef = line.startswith("#define")
  78. if not gotdef:
  79. gotdef = isdef
  80. elif not isdef:
  81. break
  82. linenum += 1
  83. currtime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  84. lines.insert(linenum, f"{prefix}#define {added:30} // Added by config.ini {currtime}\n")
  85. fullpath.write_text(''.join(lines), encoding='utf-8')
  86. # Disable all (most) defined options in the configuration files.
  87. # Everything in the named sections. Section hint for exceptions may be added.
  88. def disable_all_options():
  89. # Create a regex to match the option and capture parts of the line
  90. regex = re.compile(r'^(\s*)(#define\s+)([A-Z0-9_]+\b)(\s?)(\s*)(.*?)(\s*)(//.*)?$', re.IGNORECASE)
  91. # Disable all enabled options in both Config files
  92. for file in ("Configuration.h", "Configuration_adv.h"):
  93. fullpath = config_path(file)
  94. lines = fullpath.read_text(encoding='utf-8').split('\n')
  95. found = False
  96. for i in range(len(lines)):
  97. line = lines[i]
  98. match = regex.match(line)
  99. if match:
  100. name = match[3].upper()
  101. if name in ('CONFIGURATION_H_VERSION', 'CONFIGURATION_ADV_H_VERSION', 'CONFIG_EXAMPLES_DIR'): continue
  102. if name.startswith('_'): continue
  103. found = True
  104. # Comment out the define
  105. # TODO: Comment more lines in a multi-line define with \ continuation
  106. lines[i] = re.sub(r'^(\s*)(#define)(\s{1,3})?(\s*)', r'\1//\2 \4', line)
  107. blab(f"Disable {name}")
  108. # If the option was found, write the modified lines
  109. if found:
  110. fullpath.write_text('\n'.join(lines), encoding='utf-8')
  111. # Fetch configuration files from GitHub given the path.
  112. # Return True if any files were fetched.
  113. def fetch_example(url):
  114. if url.endswith("/"): url = url[:-1]
  115. if not url.startswith('http'):
  116. brch = "bugfix-2.1.x"
  117. if '@' in url: url, brch = map(str.strip, url.split('@'))
  118. if url == 'examples/default': url = 'default'
  119. url = f"https://raw.githubusercontent.com/MarlinFirmware/Configurations/{brch}/config/{url}"
  120. url = url.replace("%", "%25").replace(" ", "%20")
  121. # Find a suitable fetch command
  122. if shutil.which("curl") is not None:
  123. fetch = "curl -L -s -S -f -o"
  124. elif shutil.which("wget") is not None:
  125. fetch = "wget -q -O"
  126. else:
  127. blab("Couldn't find curl or wget", -1)
  128. return False
  129. import os
  130. # Reset configurations to default
  131. os.system("git checkout HEAD Marlin/*.h")
  132. # Try to fetch the remote files
  133. gotfile = False
  134. for fn in ("Configuration.h", "Configuration_adv.h", "_Bootscreen.h", "_Statusscreen.h"):
  135. if os.system(f"{fetch} wgot {url}/{fn} >/dev/null 2>&1") == 0:
  136. shutil.move('wgot', config_path(fn))
  137. gotfile = True
  138. if Path('wgot').exists(): shutil.rmtree('wgot')
  139. return gotfile
  140. def section_items(cp, sectkey):
  141. return cp.items(sectkey) if sectkey in cp.sections() else []
  142. # Apply all items from a config section. Ignore ini_ items outside of config:base and config:root.
  143. def apply_ini_by_name(cp, sect):
  144. iniok = True
  145. if sect in ('config:base', 'config:root'):
  146. iniok = False
  147. items = section_items(cp, 'config:base') + section_items(cp, 'config:root')
  148. else:
  149. items = section_items(cp, sect)
  150. for item in items:
  151. if iniok or not item[0].startswith('ini_'):
  152. apply_opt(item[0], item[1])
  153. # Apply all config sections from a parsed file
  154. def apply_all_sections(cp):
  155. for sect in cp.sections():
  156. if sect.startswith('config:'):
  157. apply_ini_by_name(cp, sect)
  158. # Apply certain config sections from a parsed file
  159. def apply_sections(cp, ckey='all'):
  160. blab(f"Apply section key: {ckey}")
  161. if ckey == 'all':
  162. apply_all_sections(cp)
  163. else:
  164. # Apply the base/root config.ini settings after external files are done
  165. if ckey in ('base', 'root'):
  166. apply_ini_by_name(cp, 'config:base')
  167. # Apply historically 'Configuration.h' settings everywhere
  168. if ckey == 'basic':
  169. apply_ini_by_name(cp, 'config:basic')
  170. # Apply historically Configuration_adv.h settings everywhere
  171. # (Some of which rely on defines in 'Conditionals-2-LCD.h')
  172. elif ckey in ('adv', 'advanced'):
  173. apply_ini_by_name(cp, 'config:advanced')
  174. # Apply a specific config:<name> section directly
  175. elif ckey.startswith('config:'):
  176. apply_ini_by_name(cp, ckey)
  177. # Apply settings from a top level config.ini
  178. def apply_config_ini(cp):
  179. blab("=" * 20 + " Gather 'config.ini' entries...")
  180. # Pre-scan for ini_use_config to get config_keys
  181. base_items = section_items(cp, 'config:base') + section_items(cp, 'config:root')
  182. config_keys = ['base']
  183. for ikey, ival in base_items:
  184. if ikey == 'ini_use_config':
  185. config_keys = map(str.strip, ival.split(','))
  186. # For each ini_use_config item perform an action
  187. for ckey in config_keys:
  188. addbase = False
  189. # For a key ending in .ini load and parse another .ini file
  190. if ckey.endswith('.ini'):
  191. sect = 'base'
  192. if '@' in ckey: sect, ckey = map(str.strip, ckey.split('@'))
  193. cp2 = configparser.ConfigParser()
  194. cp2.read(config_path(ckey), encoding='utf-8')
  195. apply_sections(cp2, sect)
  196. ckey = 'base'
  197. # (Allow 'example/' as a shortcut for 'examples/')
  198. elif ckey.startswith('example/'):
  199. ckey = 'examples' + ckey[7:]
  200. # For 'examples/<path>' fetch an example set from GitHub.
  201. # For https?:// do a direct fetch of the URL.
  202. if ckey.startswith('examples/') or ckey.startswith('http'):
  203. fetch_example(ckey)
  204. ckey = 'base'
  205. #
  206. # [flatten] Write out Configuration.h and Configuration_adv.h files with
  207. # just the enabled options and all other content removed.
  208. #
  209. #if ckey == '[flatten]':
  210. # write_flat_configs()
  211. if ckey == '[disable]':
  212. disable_all_options()
  213. elif ckey == 'all':
  214. apply_sections(cp)
  215. else:
  216. # Apply keyed sections after external files are done
  217. apply_sections(cp, 'config:' + ckey)
  218. if __name__ == "__main__":
  219. #
  220. # From command line use the given file name
  221. #
  222. import sys
  223. args = sys.argv[1:]
  224. if len(args) > 0:
  225. if args[0].endswith('.ini'):
  226. ini_file = args[0]
  227. else:
  228. print("Usage: %s <.ini file>" % os.path.basename(sys.argv[0]))
  229. else:
  230. ini_file = config_path('config.ini')
  231. if ini_file:
  232. user_ini = configparser.ConfigParser()
  233. user_ini.read(ini_file, encoding='utf-8')
  234. apply_config_ini(user_ini)
  235. else:
  236. #
  237. # From within PlatformIO use the loaded INI file
  238. #
  239. import pioutil
  240. if pioutil.is_pio_build():
  241. try:
  242. verbose = int(pioutil.env.GetProjectOption('custom_verbose'))
  243. except:
  244. pass
  245. from platformio.project.config import ProjectConfig
  246. apply_config_ini(ProjectConfig())