Welcome to mirror list, hosted at ThFree Co, Russian Federation.

create_steam_builds.py « steam « release - git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 2ecd0c347f70cc4cfa096782838fc9800cb7670d (plain)
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
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
#!/usr/bin/env python3

import argparse
import pathlib
import requests
import shutil
import subprocess
from typing import Callable, Iterator, List, Tuple

# supported archive and platform endings, used to create actual archive names
archive_endings = ["windows64.zip", "linux64.tar.xz", "macOS.dmg"]


def add_optional_argument(option: str, help: str) -> None:
    global parser
    """Add an optional argument

    Args:
        option (str): Option to add
        help (str): Help description for the argument
    """
    parser.add_argument(option, help=help, action='store_const', const=1)


def blender_archives(version: str) -> Iterator[str]:
    """Generator for Blender archives for version.

    Yields for items in archive_endings an archive name in the form of
    blender-{version}-{ending}.

    Args:
        version (str): Version string of the form 2.83.2


    Yields:
        Iterator[str]: Name in the form of blender-{version}-{ending}
    """
    global archive_endings

    for ending in archive_endings:
        yield f"blender-{version}-{ending}"


def get_archive_type(archive_type: str, version: str) -> str:
    """Return the archive of given type and version.

    Args:
        archive_type (str): extension for archive type to check for
        version (str): Version string in the form 2.83.2

    Raises:
        Exception: Execption when archive type isn't found

    Returns:
        str: archive name for given type
    """

    for archive in blender_archives(version):
        if archive.endswith(archive_type):
            return archive
    raise Exception("Unknown archive type")


def execute_command(cmd: List[str], name: str, errcode: int, cwd=".", capture_output=True) -> str:
    """Execute the given command.

    Returns the process stdout upon success if any.

    On error print message the command with name that has failed. Print stdout
    and stderr of the process if any, and then exit with given error code.

    Args:
        cmd (List[str]): Command in list format, each argument as their own item
        name (str): Name of command to use when printing to command-line
        errcode (int): Error code to use in case of exit()
        cwd (str, optional): Folder to use as current work directory for command
                             execution. Defaults to ".".
        capture_output (bool, optional): Whether to capture command output or not.
                                         Defaults to True.

    Returns:
        str: stdout if any, or empty string
    """
    cmd_process = subprocess.run(
        cmd, capture_output=capture_output, encoding="UTF-8", cwd=cwd)
    if cmd_process.returncode == 0:
        if cmd_process.stdout:
            return cmd_process.stdout
        else:
            return ""
    else:
        print(f"ERROR: {name} failed.")
        if cmd_process.stdout:
            print(cmd_process.stdout)
        if cmd_process.stderr:
            print(cmd_process.stderr)
        exit(errcode)
        return ""


def download_archives(base_url: str, archives: Callable[[str], Iterator[str]], version: str, dst_dir: pathlib.Path):
    """Download archives from the given base_url.

    Archives is a generator for Blender archive names based on version.

    Archive names are appended to the base_url to load from, and appended to
    dst_dir to save to.

    Args:
        base_url (str): Base URL to load archives from
        archives (Callable[[str], Iterator[str]]): Generator for Blender archive
                                                   names based on version
        version (str): Version string in the form of 2.83.2
        dst_dir (pathlib.Path): Download destination
    """

    if base_url[-1] != '/':
        base_url = base_url + '/'

    for archive in archives(version):
        download_url = f"{base_url}{archive}"
        target_file = dst_dir.joinpath(archive)
        download_file(download_url, target_file)


def download_file(from_url: str, to_file: pathlib.Path) -> None:
    """Download from_url as to_file.

    Actual downloading will be skipped if --skipdl is given on the command-line.

    Args:
        from_url (str): Full URL to resource to download
        to_file (pathlib.Path): Full path to save downloaded resource as
    """
    global args

    if not args.skipdl or not to_file.exists():
        print(f"Downloading {from_url}")
        with open(to_file, "wb") as download_zip:
            response = requests.get(from_url)
            if response.status_code != requests.codes.ok:
                print(f"ERROR: failed to download {from_url} (status code: {response.status_code})")
                exit(1313)
            download_zip.write(response.content)
    else:
        print(f"Downloading {from_url} skipped")
    print("   ... OK")


def copy_contents_from_dmg_to_path(dmg_file: pathlib.Path, dst: pathlib.Path) -> None:
    """Copy the contents of the given DMG file to the destination folder.

    Args:
        dmg_file (pathlib.Path): Full path to DMG archive to extract from
        dst (pathlib.Path): Full path to destination to extract to
    """
    hdiutil_attach = ["hdiutil",
                      "attach",
                      "-readonly",
                      f"{dmg_file}"
                      ]
    attached = execute_command(hdiutil_attach, "hdiutil attach", 1)

    # Last line of output is what we want, it is of the form
    # /dev/somedisk    Apple_HFS     /Volumes/Blender
    # We want to retain the mount point, and the folder the mount is
    # created on. The mounted disk we need for detaching, the folder we
    # need to be able to copy the contents to where we can use them
    attachment_items = attached.splitlines()[-1].split()
    mounted_disk = attachment_items[0]
    source_location = pathlib.Path(attachment_items[2], "Blender.app")

    print(f"{source_location} -> {dst}")

    shutil.copytree(source_location, dst)

    hdiutil_detach = ["hdiutil",
                      "detach",
                      f"{mounted_disk}"
                      ]
    execute_command(hdiutil_detach, "hdiutil detach", 2)


def create_build_script(template_name: str, vars: List[Tuple[str, str]]) -> pathlib.Path:
    """
    Create the Steam build script

    Use the given template and template variable tuple list.

    Returns pathlib.Path to the created script.

    Args:
        template_name (str): [description]
        vars (List[Tuple[str, str]]): [description]

    Returns:
        pathlib.Path: Full path to the generated script
    """
    build_script = pathlib.Path(".", template_name).read_text()
    for var in vars:
        build_script = build_script.replace(var[0], var[1])
    build_script_file = template_name.replace(".template", "")
    build_script_path = pathlib.Path(".", build_script_file)
    build_script_path.write_text(build_script)
    return build_script_path


def clean_up() -> None:
    """Remove intermediate files depending on given command-line arguments
    """
    global content_location, args

    if not args.leavearch and not args.leaveextracted:
        shutil.rmtree(content_location)

    if args.leavearch and not args.leaveextracted:
        shutil.rmtree(content_location.joinpath(zip_extract_folder))
        shutil.rmtree(content_location.joinpath(tarxz_extract_folder))
        shutil.rmtree(content_location.joinpath(dmg_extract_folder))

    if args.leaveextracted and not args.leavearch:
        import os
        os.remove(content_location.joinpath(zipped_blender))
        os.remove(content_location.joinpath(tarxz_blender))
        os.remove(content_location.joinpath(dmg_blender))


def extract_archive(archive: str, extract_folder_name: str,
                    cmd: List[str], errcode: int) -> None:
    """Extract all files from archive to given folder name.

    Will not extract if
    target folder already exists, or if --skipextract was given on the
    command-line.

    Args:
        archive (str): Archive name to extract
        extract_folder_name (str): Folder name to extract to
        cmd (List[str]): Command with arguments to use
        errcode (int): Error code to use for exit()
    """
    global args, content_location

    extract_location = content_location.joinpath(extract_folder_name)

    pre_extract = set(content_location.glob("*"))

    if not args.skipextract or not extract_location.exists():
        print(f"Extracting files from {archive}...")
        cmd.append(content_location.joinpath(archive))
        execute_command(cmd, cmd[0], errcode, cwd=content_location)
        # in case we use a non-release archive the naming will be incorrect.
        # simply rename to expected target name
        post_extract = set(content_location.glob("*"))
        diff_extract = post_extract - pre_extract
        if not extract_location in diff_extract:
            folder_to_rename = list(diff_extract)[0]
            folder_to_rename.rename(extract_location)
        print("   OK")
    else:
        print(f"Skipping extraction {archive}!")

# ==============================================================================


parser = argparse.ArgumentParser()

parser.add_argument("--baseurl", required=True,
                    help="The base URL for files to download, "
                    "i.e. https://download.blender.org/release/Blender2.83/")

parser.add_argument("--version", required=True,
                    help="The Blender version to release, in the form 2.83.3")

parser.add_argument("--appid", required=True,
                    help="The Blender App ID on Steam")
parser.add_argument("--winid", required=True,
                    help="The Windows depot ID")
parser.add_argument("--linuxid", required=True,
                    help="The Linux depot ID")
parser.add_argument("--macosid", required=True,
                    help="The MacOS depot ID")

parser.add_argument("--steamcmd", required=True,
                    help="Path to the steamcmd")
parser.add_argument("--steamuser", required=True,
                    help="The login for the Steam builder user")
parser.add_argument("--steampw", required=True,
                    help="Login password for the Steam builder user")

add_optional_argument("--dryrun",
                      "If set the Steam files will not be uploaded")
add_optional_argument("--leavearch",
                      help="If set don't clean up the downloaded archives")
add_optional_argument("--leaveextracted",
                      help="If set don't clean up the extraction folders")
add_optional_argument("--skipdl",
                      help="If set downloading the archives is skipped if it already exists locally.")
add_optional_argument("--skipextract",
                      help="If set skips extracting of archives. The tool assumes the archives"
                      "have already been extracted to their correct locations")

args = parser.parse_args()

VERSIONNODOTS = args.version.replace('.', '')
OUTPUT = f"output{VERSIONNODOTS}"
CONTENT = f"content{VERSIONNODOTS}"

# ===== set up main locations

content_location = pathlib.Path(".", CONTENT).absolute()
output_location = pathlib.Path(".", OUTPUT).absolute()

content_location.mkdir(parents=True, exist_ok=True)
output_location.mkdir(parents=True, exist_ok=True)

# ===== login

# Logging into Steam once to ensure the SDK updates itself properly. If we don't
# do that the combined +login and +run_app_build_http at the end of the tool
# will fail.
steam_login = [args.steamcmd,
               "+login",
               args.steamuser,
               args.steampw,
               "+quit"
               ]
print("Logging in to Steam...")
execute_command(steam_login, "Login to Steam", 10)
print("   OK")

# ===== prepare Steam build scripts

template_vars = [
    ("[APPID]", args.appid),
    ("[OUTPUT]", OUTPUT),
    ("[CONTENT]", CONTENT),
    ("[VERSION]", args.version),
    ("[WINID]", args.winid),
    ("[LINUXID]", args.linuxid),
    ("[MACOSID]", args.macosid),
    ("[DRYRUN]", f"{args.dryrun}" if args.dryrun else "0")
]

blender_app_build = create_build_script(
    "blender_app_build.vdf.template", template_vars)
create_build_script("depot_build_win.vdf.template", template_vars)
create_build_script("depot_build_linux.vdf.template", template_vars)
create_build_script("depot_build_macos.vdf.template", template_vars)

# ===== download archives

download_archives(args.baseurl, blender_archives,
                  args.version, content_location)

# ===== set up file and folder names

zipped_blender = get_archive_type("zip", args.version)
zip_extract_folder = zipped_blender.replace(".zip", "")
tarxz_blender = get_archive_type("tar.xz", args.version)
tarxz_extract_folder = tarxz_blender.replace(".tar.xz", "")
dmg_blender = get_archive_type("dmg", args.version)
dmg_extract_folder = dmg_blender.replace(".dmg", "")

# ===== extract

unzip_cmd = ["unzip", "-q"]
extract_archive(zipped_blender, zip_extract_folder,  unzip_cmd, 3)

untarxz_cmd = ["tar", "-xf"]
extract_archive(tarxz_blender, tarxz_extract_folder, untarxz_cmd, 4)

if not args.skipextract or not content_location.joinpath(dmg_extract_folder).exists():
    print("Extracting files from Blender MacOS archive...")
    blender_dmg = content_location.joinpath(dmg_blender)
    target_location = content_location.joinpath(
        dmg_extract_folder, "Blender.app")
    copy_contents_from_dmg_to_path(blender_dmg, target_location)
    print("   OK")
else:
    print("Skipping extraction of .dmg!")

# ===== building

print("Build Steam game files...")
steam_build = [args.steamcmd,
               "+login",
               args.steamuser,
               args.steampw,
               "+run_app_build_http",
               blender_app_build.absolute(),
               "+quit"
               ]
execute_command(steam_build, "Build with steamcmd", 13)
print("   OK")

clean_up()