1+ # /// script
2+ # dependencies = [
3+ # "Jinja2",
4+ # ]
5+ # ///
6+ import argparse
7+ import enum
8+ import hashlib
9+ import os
10+ import sys
11+ import tarfile
12+ import tomllib
13+ from dataclasses import dataclass , field , asdict
14+ from typing import TypedDict , List
15+
16+ import jinja2
17+
18+ TEMPLATE_FILE = os .path .join (os .path .dirname (__file__ ),"tripwire_formula.rb.jinja2" )
19+
20+ class GitInfo (TypedDict ):
21+ head : str
22+ branch : str
23+
24+ class DependencyType (enum .Enum ):
25+ BUILD = 'build'
26+ INSTALL = 'install'
27+ TEST = 'test'
28+
29+ @dataclass
30+ class Dependency :
31+ dependency : str
32+ dependency_type : DependencyType = DependencyType .INSTALL
33+
34+ @dataclass
35+ class Resource :
36+ name : str
37+ url : str
38+ sha256 : str
39+
40+ @dataclass
41+ class FormulaInfo :
42+ name : str
43+ desc : str
44+ homepage : str
45+ url : str
46+ sha256 : str
47+ license : str
48+ git_info : GitInfo
49+ resources : List [Resource ] = field (default_factory = list )
50+ depends_on : List [Dependency ] = field (default_factory = list )
51+
52+
53+ def read_pkg_info_fp (file_pointer ):
54+ data = {
55+ "name" : None ,
56+ "version" : None ,
57+ "license" : None ,
58+ "summary" : None ,
59+ "project_url" : None
60+ }
61+ for line in file_pointer .read ().decode ("utf-8" ).split ("\n " ):
62+ match line .split ():
63+ case ["Name:" , name ]:
64+ data ["name" ] = name
65+
66+ case ["Version:" , version ]:
67+ data ["version" ] = version
68+
69+ case ["License-Expression:" , license_type ]:
70+ data ["license" ] = license_type
71+
72+ case ["Summary:" , * values ]:
73+ data ["summary" ] = " " .join (values )
74+
75+ case ["Project-URL:" , "project," , url ]:
76+ data ["project_url" ] = url
77+ return data
78+
79+ def get_package_info (tarball_location ):
80+ metadata = {
81+ "sha256" : None
82+ }
83+
84+ with open (tarball_location , 'rb' , buffering = 0 ) as f :
85+ metadata ['sha256' ] = hashlib .file_digest (f , "sha256" ).hexdigest ()
86+
87+ with tarfile .open (tarball_location , "r:gz" ) as tar :
88+ for member in tar .getmembers ():
89+ location , name = os .path .split (member .name )
90+ if member .isdir ():
91+ continue
92+
93+ # PKG-INFO is found one level deep in something like uiucprescon_tripwire-0.3.8
94+ if len (location .split (os .sep )) > 1 :
95+ continue
96+ if name != "PKG-INFO" :
97+ continue
98+ with tar .extractfile (member ) as f :
99+ return {** metadata , ** read_pkg_info_fp (f )}
100+
101+ raise ValueError ("no PKG-INFO found" )
102+
103+ def get_lock_file_packages (lockfile_path ):
104+ packages = {}
105+ with open (lockfile_path , "rb" ) as f :
106+ data = tomllib .load (f )
107+
108+ for package in data ['packages' ]:
109+
110+ if 'marker' in package :
111+ if "sys_platform == 'win32'" in package ['marker' ]:
112+ continue
113+ if "sys_platform == 'linux'" in package ['marker' ]:
114+ continue
115+
116+ if "directory" in package :
117+ continue
118+
119+ packages [package ['name' ]] = {
120+ k : v for k , v in package .items () if k not in ("name" , "wheels" )
121+ }
122+ return packages
123+
124+ def render_formula (data ) -> str :
125+ with open (TEMPLATE_FILE ) as f :
126+ template = jinja2 .Template (
127+ f .read (),
128+ )
129+ template .globals ["DependencyType" ] = DependencyType
130+ return template .render (** asdict (data ))
131+
132+ def get_args ():
133+ parser = argparse .ArgumentParser ()
134+
135+ parser .add_argument (
136+ "sdist" ,
137+ help = "path to tar.gz sdist file. "
138+ "For example: uiucprescon_tripwire-0.3.8.tar.gz"
139+ )
140+
141+ parser .add_argument (
142+ "lockfile" ,
143+ help = "path to pylock.toml lockfile. For example pylock.toml" ,
144+ )
145+
146+ parser .add_argument ("url" , help = "url to sdist package stored on internet" )
147+
148+ return parser
149+
150+ def validate_args (args ):
151+ issues = []
152+
153+ if not os .path .isfile (args .sdist ):
154+ issues .append (f"sdist does not exist" )
155+
156+ if not os .path .isfile (args .lockfile ):
157+ issues .append (f"lockfile does not exist" )
158+
159+ return issues
160+
161+ def main ():
162+ args = get_args ().parse_args ()
163+
164+ if issues := validate_args (args ):
165+ sys .stdout .flush ()
166+ for issue in issues :
167+ print (issue , file = sys .stderr )
168+ exit (1 )
169+
170+ info = get_package_info (args .sdist )
171+ lockfile_packages = get_lock_file_packages (args .lockfile )
172+
173+ print (
174+ render_formula (
175+ FormulaInfo (
176+ name = "Tripwire" ,
177+ desc = info ['summary' ],
178+ homepage = info ['project_url' ],
179+ url = args .url ,
180+ sha256 = info ['sha256' ],
181+ license = info ['license' ],
182+ git_info = {
183+ "head" : "https://github.com/UIUCLibrary/tripwire.git" ,
184+ "branch" : "main"
185+ },
186+ depends_on = [
187+ Dependency ("conan" , DependencyType .BUILD ),
188+ Dependency ("python@3.14" ),
189+ ],
190+ resources = sorted (
191+ [
192+ Resource (
193+ name = pkg_name ,
194+ url = pkg_data ['sdist' ]['url' ],
195+ sha256 = pkg_data ['sdist' ]['hashes' ]['sha256' ]
196+ ) for pkg_name , pkg_data in lockfile_packages .items ()
197+ ],
198+ key = lambda r : r .name
199+ )
200+ )
201+ )
202+ )
203+
204+ if __name__ == '__main__' :
205+ main ()
0 commit comments