File size: 10,859 Bytes
b66f552
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
#!/usr/bin/env python3
"""Build split packages with proper dependency management and copy them to target directory."""

import ast
import re
import shutil
import subprocess
import sys
from pathlib import Path


def extract_dependencies():
    """Extract dependencies from setup.py using AST."""
    # Get script directory and find setup.py in parent directory
    script_dir = Path(__file__).parent
    setup_py = script_dir.parent / 'setup.py'

    with open(setup_py, encoding='utf-8') as f:
        tree = ast.parse(f.read(), filename=str(setup_py))

    all_deps = []
    extras = {}

    for node in ast.walk(tree):
        if isinstance(node, ast.Call) and getattr(node.func, 'id', '') == 'setup':
            for keyword in node.keywords:
                if (keyword.arg == 'install_requires' and
                        isinstance(keyword.value, (ast.List, ast.Tuple))):
                    all_deps.extend([
                        elt.value for elt in keyword.value.elts
                        if isinstance(elt, ast.Constant) and isinstance(elt.value, str)
                    ])
                elif (keyword.arg == 'extras_require' and
                      isinstance(keyword.value, ast.Dict)):
                    for key_node, val_node in zip(keyword.value.keys, keyword.value.values, strict=False):
                        if (isinstance(key_node, ast.Constant) and
                            isinstance(key_node.value, str) and
                                isinstance(val_node, (ast.List, ast.Tuple))):
                            key = key_node.value
                            values = [
                                elt.value for elt in val_node.elts
                                if isinstance(elt, ast.Constant) and isinstance(elt.value, str)
                            ]
                            extras[key] = values
            break  # Assume only one setup() call

    return all_deps, extras


def categorize_dependencies(deps):
    """Categorize dependencies based on core vs extension."""
    core_deps = []
    ext_deps = []

    for dep in deps:
        if any(core in dep for core in ['torch', 'einops']):
            core_deps.append(dep)
        else:
            ext_deps.append(dep)

    return core_deps, ext_deps


def create_pyproject_toml(package_dir, name, version, dependencies, extras=None):
    """Create pyproject.toml for a package."""
    if extras is None:
        extras = {}

    extras_content = ""
    if extras:
        extras_content = "\n[project.optional-dependencies]\n"
        for key, values in extras.items():
            values_str = ', '.join(f'"{v}"' for v in values)
            extras_content += f"{key} = [{values_str}]\n"

    deps_content = ', '.join(f'"{dep}"' for dep in dependencies)

    # Create description text
    if name == 'fla-core':
        desc_text = 'Core operations for flash-linear-attention'
    else:
        desc_text = 'Fast linear attention models and layers'

    content = f"""[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "{name}"
version = "{version}"
description = "{desc_text}"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [{deps_content}]

[project.urls]
Homepage = "https://github.com/fla-org/flash-linear-attention"
Repository = "https://github.com/fla-org/flash-linear-attention"
"""

    content += extras_content

    # Add setuptools namespace package configuration for extension package
    if name == 'flash-linear-attention':
        content += """

[tool.setuptools.packages.find]
include = ["fla*"]
namespaces = true
"""

    with open(package_dir / 'pyproject.toml', 'w') as f:
        f.write(content)


def build_split_packages():
    """Build split packages with proper dependency management."""
    # Get script directory and find files relative to it
    script_dir = Path(__file__).parent
    root_dir = script_dir.parent

    # Get current version
    init_file = root_dir / 'fla' / '__init__.py'
    with open(init_file, encoding='utf-8') as f:
        content = f.read()
    version_match = re.search(r"^__version__\s*=\s*['\"]([^'\"]+)['\"]\s*$", content, re.MULTILINE)
    if not version_match:
        raise RuntimeError(f"Could not find __version__ in {init_file}")
    version = version_match.group(1)

    # Extract dependencies
    all_deps, extras = extract_dependencies()
    core_deps, ext_deps = categorize_dependencies(all_deps)

    # Add version constraint for fla-core in extension package
    ext_deps.insert(0, f'fla-core=={version}')

    # Create output directory
    output_dir = script_dir / 'dist'
    output_dir.mkdir(exist_ok=True)

    # Create fla-core package
    core_dir = output_dir / 'fla-core'
    if core_dir.exists():
        shutil.rmtree(core_dir)
    core_dir.mkdir()

    # Copy core files
    fla_core = core_dir / 'fla'
    shutil.copytree(root_dir / 'fla' / 'ops', fla_core / 'ops')
    shutil.copytree(root_dir / 'fla' / 'modules', fla_core / 'modules')
    shutil.copy(root_dir / 'fla' / 'utils.py', fla_core / 'utils.py')

    # Create fla-core __init__.py
    with open(fla_core / '__init__.py', 'w') as f:
        f.write(f"""# -*- coding: utf-8 -*-

__path__ = __import__('pkgutil').extend_path(__path__, __name__)
__version__ = '{version}'
""")

    # Copy ancillary files (README.md, LICENSE) to core package
    for fname in ("README.md", "LICENSE"):
        src = root_dir / fname
        if src.exists():
            shutil.copy(src, core_dir / fname)

    # Create fla-core configs
    create_pyproject_toml(core_dir, 'fla-core', version, core_deps)

    # Create flash-linear-attention package
    ext_dir = output_dir / 'flash-linear-attention'
    if ext_dir.exists():
        shutil.rmtree(ext_dir)
    ext_dir.mkdir()

    # Copy extension files
    fla_ext = ext_dir / 'fla'
    shutil.copytree(root_dir / 'fla' / 'models', fla_ext / 'models')
    shutil.copytree(root_dir / 'fla' / 'layers', fla_ext / 'layers')

    # Intentionally do NOT create fla/__init__.py in the extension package.
    # The top-level package is provided by fla-core (namespace via pkgutil).

    # Copy ancillary files (README.md, LICENSE) to extension package
    for fname in ("README.md", "LICENSE"):
        src = root_dir / fname
        if src.exists():
            shutil.copy(src, ext_dir / fname)

    # Create extension configs
    create_pyproject_toml(ext_dir, 'flash-linear-attention', version, ext_deps, extras)

    # Create build script
    build_script = output_dir / 'build.sh'
    with open(build_script, 'w') as f:
        f.write("""#!/bin/bash
# Build both packages

echo "Building fla-core..."
cd fla-core
pip install -U build
python -m build

echo "Building flash-linear-attention..."
cd ../flash-linear-attention
python -m build

echo "Build complete! Packages in dist/"
""")

    build_script.chmod(0o755)

    print(f"✅ Split packages created in {output_dir}")
    print(f"✅ fla-core dependencies: {len(core_deps)} packages")
    print(f"✅ flash-linear-attention dependencies: {len(ext_deps)} packages")
    print(f"✅ Version: {version}")

    return output_dir, version


def build_packages(dist_dir):
    """Build wheels and source distributions for both packages."""
    print("Building packages...")

    # Build fla-core (both wheel and sdist)
    print("Building fla-core packages...")
    try:
        subprocess.run(
            [sys.executable, "-m", "build", str(dist_dir / "fla-core")],
            check=True,
            timeout=1800,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            text=True,
        )
    except subprocess.CalledProcessError as e:
        print("Failed to build fla-core packages:")
        print(e.stdout)
        return False
    except subprocess.TimeoutExpired:
        print("Timed out building fla-core packages")
        return False

    # Build flash-linear-attention (both wheel and sdist)
    print("Building flash-linear-attention packages...")
    try:
        subprocess.run(
            [sys.executable, "-m", "build", str(dist_dir / "flash-linear-attention")],
            check=True,
            timeout=1800,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            text=True,
        )
    except subprocess.CalledProcessError as e:
        print("Failed to build flash-linear-attention packages:")
        print(e.stdout)
        return False
    except subprocess.TimeoutExpired:
        print("Timed out building flash-linear-attention packages")
        return False

    print("✅ Packages built successfully")
    return True


def copy_packages_to_output(dist_dir):
    """Copy wheels and source distributions to output directory."""
    # Get script directory (relative to this file)
    script_dir = Path(__file__).parent
    root_dir = script_dir.parent

    # Create output directory (relative to root)
    output_dir = root_dir / 'dist-packages'
    output_dir.mkdir(exist_ok=True)

    # Find wheels and source distributions
    core_wheels = list((dist_dir / 'fla-core' / 'dist').glob('*.whl'))
    core_sdist = list((dist_dir / 'fla-core' / 'dist').glob('*.tar.gz'))
    ext_wheels = list((dist_dir / 'flash-linear-attention' / 'dist').glob('*.whl'))
    ext_sdist = list((dist_dir / 'flash-linear-attention' / 'dist').glob('*.tar.gz'))

    if not core_wheels:
        print("No fla-core wheel found")
        return False
    if not ext_wheels:
        print("No flash-linear-attention wheel found")
        return False

    # Copy all packages to output directory
    all_packages = core_wheels + core_sdist + ext_wheels + ext_sdist
    for package in all_packages:
        target = output_dir / package.name
        shutil.copy2(package, target)
        if package.suffix == ".whl":
            package_type = "wheel"
        elif package.suffixes[-2:] == [".tar", ".gz"]:
            package_type = "sdist"
        else:
            package_type = "source"
        print(f"📦 Copied {package_type} package {package.name} to {output_dir}")

    print(f"\n✅ All packages copied to: {output_dir}")
    print("You can install wheels with:")
    print("  pip install dist-packages/*.whl")
    print("Source distributions are also available in:", output_dir)

    return True


def main():
    """Build split packages and copy to target directory."""

    print("Building split packages...")

    # Build the split packages
    dist_dir, _ = build_split_packages()

    print("\nTo build packages manually:")
    print(f"cd {dist_dir}")
    print("./build.sh")

    # Build packages (wheels and source distributions)
    if not build_packages(dist_dir):
        return 1

    # Copy packages to output directory
    if not copy_packages_to_output(dist_dir):
        return 1

    return 0


if __name__ == "__main__":
    exit(main())