Skip to content

Commit f66ca99

Browse files
authored
Added support for assembling on Windows. (#809)
* Added support for assembling on Windows. Signed-off-by: dblock <[email protected]> * Use platform provided. Signed-off-by: dblock <[email protected]>
1 parent fba6709 commit f66ca99

20 files changed

+2717
-116
lines changed

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@
2828
- [Making a Release](#making-a-release)
2929
- [Releasing for Linux](#releasing-for-linux)
3030
- [Releasing for FreeBSD](#releasing-for-freebsd)
31-
- [Deploying infrastructure](#deploying-infrastructure)
31+
- [Releasing for Windows](#releasing-for-windows)
32+
- [Releasing for MacOS](#releasing-for-macos)
33+
- [Deploying Infrastructure](#deploying-infrastructure)
3234
- [Contributing](#contributing)
3335
- [Getting Help](#getting-help)
3436
- [Code of Conduct](#code-of-conduct)
@@ -353,7 +355,15 @@ The Linux release is managed by a team at Amazon following [this release templat
353355

354356
The FreeBSD ports and packages for OpenSearch are managed by a community [OpenSearch Team](https://wiki.freebsd.org/OpenSearch) at FreeBSD. When a new release is rolled out, this team will update the port and commit it to the FreeBSD ports tree. Anybody is welcome to help the team by providing patches for [upgrading the ports](https://docs.freebsd.org/en/books/porters-handbook/book/#port-upgrading) following the [FreeBSD Porter's Handbook](https://docs.freebsd.org/en/books/porters-handbook/book/) instructions.
355357

356-
### Deploying infrastructure
358+
#### Releasing for Windows
359+
360+
At this moment there's no official Windows distribution. However, this project does support building and assembling OpenSearch for Windows, with some caveats. See [opensearch-build#33](https://github.com/opensearch-project/opensearch-build/issues/33) for details.
361+
362+
#### Releasing for MacOS
363+
364+
At this moment there's no official MacOS distribution. However, this project does support building and assembling OpenSearch for MacOS. See [opensearch-build#37](https://github.com/opensearch-project/opensearch-build/issues/37) and [#38](https://github.com/opensearch-project/opensearch-build/issues/38) for more details.
365+
366+
### Deploying Infrastructure
357367

358368
Storage and access roles for the OpenSearch release process are codified in a [CDK project](deployment/README.md).
359369

manifests/1.2.0/opensearch-1.2.0.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ components:
3535
- name: k-NN
3636
repository: https://github.com/opensearch-project/k-NN.git
3737
ref: "main"
38+
platforms:
39+
- darwin
40+
- linux
3841
checks:
3942
- gradle:properties:version
4043
- gradle:dependencies:opensearch.version

scripts/components/OpenSearch/build.sh

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,25 +78,21 @@ cp -r ./build/local-test-repo/org/opensearch "${OUTPUT}"/maven/org
7878
[ -z "$PLATFORM" ] && PLATFORM=`uname -s` | awk '{print tolower($0)}'
7979
[ -z "$ARCHITECTURE" ] && ARCHITECTURE=`uname -m`
8080

81-
case "$(uname -s)" in
82-
Linux*)
81+
case $PLATFORM in
82+
linux*)
8383
PACKAGE="tar"
8484
EXT="tar.gz"
8585
;;
86-
Darwin*)
86+
darwin*)
8787
PACKAGE="tar"
8888
EXT="tar.gz"
8989
;;
90-
CYGWIN*)
91-
PACKAGE="zip"
92-
EXT="zip"
93-
;;
94-
MINGW*)
90+
windows*)
9591
PACKAGE="zip"
9692
EXT="zip"
9793
;;
9894
*)
99-
echo "Unsupported system: $(uname -s)"
95+
echo "Unsupported platform: $PLATFORM"
10096
exit 1
10197
;;
10298
esac
@@ -111,7 +107,7 @@ case $ARCHITECTURE in
111107
QUALIFIER="$PLATFORM-arm64"
112108
;;
113109
*)
114-
echo "Unsupported architecture: ${ARCHITECTURE}"
110+
echo "Unsupported architecture: $ARCHITECTURE"
115111
exit 1
116112
;;
117113
esac

src/assemble_workflow/bundle.py

Lines changed: 21 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
import os
1010
import shutil
1111
import subprocess
12-
import tarfile
1312
from abc import ABC, abstractmethod
1413

14+
from assemble_workflow.dist import Dist
1515
from paths.script_finder import ScriptFinder
1616
from system.temporary_directory import TemporaryDirectory
1717

@@ -23,69 +23,56 @@
2323

2424

2525
class Bundle(ABC):
26+
def __enter__(self):
27+
return self
28+
29+
def __exit__(self, exc_type, exc_value, exc_traceback):
30+
self.tmp_dir.__exit__(exc_type, exc_value, exc_traceback)
31+
2632
def __init__(self, build_manifest, artifacts_dir, bundle_recorder):
2733
"""
2834
Construct a new Bundle instance.
2935
:param build_manifest: A BuildManifest created from the build workflow.
3036
:param artifacts_dir: Dir location where build artifacts can be found locally
3137
:param bundle_recorder: The bundle recorder that will capture and build a BundleManifest
3238
"""
33-
self.min_tarball = self.__get_min_bundle(build_manifest.components.values())
3439
self.plugins = self.__get_plugins(build_manifest.components.values())
3540
self.artifacts_dir = artifacts_dir
3641
self.bundle_recorder = bundle_recorder
3742
self.tmp_dir = TemporaryDirectory()
43+
self.min_dist = self.__get_min_dist(build_manifest.components.values())
3844
self.installed_plugins = []
39-
self.min_tarball_path = self._copy_component(self.min_tarball, "dist")
40-
self.__unpack_min_tarball(self.tmp_dir.name)
4145

4246
def install_min(self):
43-
post_install_script = ScriptFinder.find_install_script(self.min_tarball.name)
44-
self._execute(f'{post_install_script} -a "{self.artifacts_dir}" -o "{self.archive_path}"')
47+
post_install_script = ScriptFinder.find_install_script(self.min_dist.name)
48+
self._execute(f'bash {post_install_script} -a "{self.artifacts_dir}" -o "{self.min_dist.archive_path}"')
4549

4650
def install_plugins(self):
4751
for plugin in self.plugins:
4852
logging.info(f"Installing {plugin.name}")
4953
self.install_plugin(plugin)
50-
plugins_path = os.path.join(self.archive_path, "plugins")
54+
plugins_path = os.path.join(self.min_dist.archive_path, "plugins")
5155
if os.path.isdir(plugins_path):
5256
self.installed_plugins = os.listdir(plugins_path)
5357

5458
@abstractmethod
5559
def install_plugin(self, plugin):
5660
post_install_script = ScriptFinder.find_install_script(plugin.name)
57-
self._execute(f'{post_install_script} -a "{self.artifacts_dir}" -o "{self.archive_path}"')
61+
self._execute(f'bash {post_install_script} -a "{self.artifacts_dir}" -o "{self.min_dist.archive_path}"')
5862

59-
def build_tar(self, dest):
60-
tar_name = self.bundle_recorder.tar_name
61-
with tarfile.open(tar_name, "w:gz") as tar:
62-
tar.add(self.archive_path, arcname=os.path.basename(self.archive_path))
63-
shutil.copyfile(tar_name, os.path.join(dest, tar_name))
63+
def package(self, dest):
64+
self.min_dist.build(self.bundle_recorder.package_name, dest)
6465

6566
def _execute(self, command):
66-
logging.info(f'Executing "{command}" in {self.archive_path}')
67-
subprocess.check_call(command, cwd=self.archive_path, shell=True)
67+
logging.info(f'Executing "{command}" in {self.min_dist.archive_path}')
68+
subprocess.check_call(command, cwd=self.min_dist.archive_path, shell=True)
6869

6970
def _copy_component(self, component, component_type):
7071
rel_path = self.__get_rel_path(component, component_type)
7172
tmp_path = self.__copy_component_files(rel_path, self.tmp_dir.name)
7273
self.bundle_recorder.record_component(component, rel_path)
7374
return tmp_path
7475

75-
def __unpack_min_tarball(self, dest):
76-
with tarfile.open(self.min_tarball_path) as tar:
77-
tar.extractall(dest)
78-
79-
self.archive_path = self.__get_archive_path(dest)
80-
81-
# OpenSearch & Dashboard tars will include only a single folder at the top level of the tar.
82-
def __get_archive_path(self, dest):
83-
for file in os.scandir(dest):
84-
if file.is_dir():
85-
return file.path
86-
87-
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), os.path.join(dest, "*"))
88-
8976
def __get_rel_path(self, component, component_type):
9077
return next(iter(component.artifacts.get(component_type, [])), None)
9178

@@ -102,8 +89,11 @@ def __copy_component_files(self, rel_path, dest):
10289
def __get_plugins(self, build_components):
10390
return [c for c in build_components if "plugins" in c.artifacts]
10491

105-
def __get_min_bundle(self, build_components):
92+
def __get_min_dist(self, build_components):
10693
min_bundle = next(iter([c for c in build_components if "dist" in c.artifacts]), None)
10794
if min_bundle is None:
10895
raise ValueError('Missing min "dist" in input artifacts.')
109-
return min_bundle
96+
min_dist_path = self._copy_component(min_bundle, "dist")
97+
min_dist = Dist.from_path(min_bundle.name, min_dist_path)
98+
min_dist.extract(self.tmp_dir.name)
99+
return min_dist

src/assemble_workflow/bundle_opensearch.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@
77
import os
88

99
from assemble_workflow.bundle import Bundle
10+
from system.os import current_platform
1011

1112

1213
class BundleOpenSearch(Bundle):
14+
@property
15+
def install_plugin_script(self):
16+
return "opensearch-plugin.bat" if current_platform() == "windows" else "opensearch-plugin"
17+
1318
def install_plugin(self, plugin):
1419
tmp_path = self._copy_component(plugin, "plugins")
15-
cli_path = os.path.join(self.archive_path, "bin", "opensearch-plugin")
20+
cli_path = os.path.join(self.min_dist.archive_path, "bin", self.install_plugin_script)
1621
self._execute(f"{cli_path} install --batch file:{tmp_path}")
1722
super().install_plugin(plugin)

src/assemble_workflow/bundle_opensearch_dashboards.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@
1212
class BundleOpenSearchDashboards(Bundle):
1313
def install_plugin(self, plugin):
1414
tmp_path = self._copy_component(plugin, "plugins")
15-
cli_path = os.path.join(self.archive_path, "bin", "opensearch-dashboards-plugin")
15+
cli_path = os.path.join(self.min_dist.archive_path, "bin", "opensearch-dashboards-plugin")
1616
self._execute(f"{cli_path} --allow-root install file:{tmp_path}")
1717
super().install_plugin(plugin)

src/assemble_workflow/bundle_recorder.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def __init__(self, build, output_dir, artifacts_dir, base_url):
1616
self.build_id = build.id
1717
self.base_url = base_url
1818
self.version = build.version
19-
self.tar_name = self.__get_tar_name(build)
19+
self.package_name = self.__get_package_name(build)
2020
self.artifacts_dir = artifacts_dir
2121
self.architecture = build.architecture
2222
self.bundle_manifest = self.BundleManifestBuilder(
@@ -25,17 +25,17 @@ def __init__(self, build, output_dir, artifacts_dir, base_url):
2525
build.version,
2626
build.platform,
2727
build.architecture,
28-
self.__get_tar_location(),
28+
self.__get_package_location(),
2929
)
3030

31-
def __get_tar_name(self, build):
31+
def __get_package_name(self, build):
3232
parts = [
3333
build.name.lower().replace(" ", "-"),
3434
build.version,
3535
build.platform,
3636
build.architecture,
3737
]
38-
return "-".join(parts) + ".tar.gz"
38+
return "-".join(parts) + (".zip" if build.platform == "windows" else ".tar.gz")
3939

4040
def __get_public_url_path(self, folder, rel_path):
4141
path = "/".join((folder, rel_path))
@@ -48,8 +48,8 @@ def __get_location(self, folder_name, rel_path, abs_path):
4848

4949
# Assembled bundles are expected to be served from a separate "bundles" folder
5050
# Example: https://artifacts.opensearch.org/bundles/1.0.0/<build-id
51-
def __get_tar_location(self):
52-
return self.__get_location("dist", self.tar_name, os.path.join(self.output_dir, self.tar_name))
51+
def __get_package_location(self):
52+
return self.__get_location("dist", self.package_name, os.path.join(self.output_dir, self.package_name))
5353

5454
# Build artifacts are expected to be served from a "builds" folder
5555
# Example: https://artifacts.opensearch.org/builds/1.0.0/<build-id>

src/assemble_workflow/dist.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
#
3+
# The OpenSearch Contributors require contributions made to
4+
# this file be licensed under the Apache-2.0 license or a
5+
# compatible open source license.
6+
7+
import errno
8+
import logging
9+
import os
10+
import shutil
11+
import tarfile
12+
import zipfile
13+
from abc import ABC, abstractmethod
14+
15+
16+
class Dist(ABC):
17+
def __init__(self, name, path):
18+
self.name = name
19+
self.path = path
20+
21+
@abstractmethod
22+
def __extract__(self, dest):
23+
pass
24+
25+
def extract(self, dest):
26+
self.__extract__(dest)
27+
28+
# OpenSearch & Dashboard tars will include only a single folder at the top level of the tar.
29+
30+
for file in os.scandir(dest):
31+
if file.is_dir():
32+
self.archive_path = file.path
33+
return self.archive_path
34+
35+
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), os.path.join(dest, "*"))
36+
37+
def build(self, name, dest):
38+
self.__build__(name, dest)
39+
path = os.path.join(dest, name)
40+
shutil.copyfile(name, path)
41+
logging.info(f"Published {path}.")
42+
43+
@classmethod
44+
def from_path(cls, name, path):
45+
ext = os.path.splitext(path)[1]
46+
if ext == ".gz":
47+
return DistTar(name, path)
48+
elif ext == ".zip":
49+
return DistZip(name, path)
50+
else:
51+
raise ValueError(f'Invalid min "dist" extension in input artifacts: {ext} ({path}).')
52+
53+
54+
class DistZip(Dist):
55+
def __extract__(self, dest):
56+
with zipfile.ZipFile(self.path, "r") as zip:
57+
zip.extractall(dest)
58+
59+
def __build__(self, name, dest):
60+
with zipfile.ZipFile(name, "w", zipfile.ZIP_DEFLATED) as zip:
61+
rootlen = len(self.archive_path) + 1
62+
for base, dirs, files in os.walk(self.archive_path):
63+
for file in files:
64+
fn = os.path.join(base, file)
65+
zip.write(fn, fn[rootlen:])
66+
67+
68+
class DistTar(Dist):
69+
def __extract__(self, dest):
70+
with tarfile.open(self.path, "r") as tar:
71+
tar.extractall(dest)
72+
73+
def __build__(self, name, dest):
74+
with tarfile.open(name, "w:gz") as tar:
75+
tar.add(self.archive_path, arcname=os.path.basename(self.archive_path))

src/run_assemble.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def main():
3030
const=logging.DEBUG,
3131
dest="logging_level",
3232
)
33-
parser.add_argument("-b", "--base-url", dest='base_url', help="The base url to download the artifacts.")
33+
parser.add_argument("-b", "--base-url", dest="base_url", help="The base url to download the artifacts.")
3434
args = parser.parse_args()
3535

3636
console.configure(level=args.logging_level)
@@ -46,17 +46,16 @@ def main():
4646

4747
bundle_recorder = BundleRecorder(build, output_dir, artifacts_dir, args.base_url)
4848

49-
bundle = Bundles.create(build_manifest, artifacts_dir, bundle_recorder)
49+
with Bundles.create(build_manifest, artifacts_dir, bundle_recorder) as bundle:
50+
bundle.install_min()
51+
bundle.install_plugins()
52+
logging.info(f"Installed plugins: {bundle.installed_plugins}")
5053

51-
bundle.install_min()
52-
bundle.install_plugins()
53-
logging.info(f"Installed plugins: {bundle.installed_plugins}")
54+
# Save a copy of the manifest inside of the tar
55+
bundle_recorder.write_manifest(bundle.min_dist.archive_path)
56+
bundle.package(output_dir)
5457

55-
# Save a copy of the manifest inside of the tar
56-
bundle_recorder.write_manifest(bundle.archive_path)
57-
bundle.build_tar(output_dir)
58-
59-
bundle_recorder.write_manifest(output_dir)
58+
bundle_recorder.write_manifest(output_dir)
6059

6160
logging.info("Done.")
6261

tests/test_run_assemble.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,23 @@ def test_usage(self, *mocks):
3333
@patch("os.makedirs")
3434
@patch("os.getcwd", return_value="curdir")
3535
@patch("argparse._sys.argv", ["run_assemble.py", BUILD_MANIFEST])
36-
@patch("run_assemble.Bundles", return_value=MagicMock())
36+
@patch("run_assemble.Bundles.create")
3737
@patch("run_assemble.BundleRecorder", return_value=MagicMock())
3838
@patch("run_assemble.TemporaryDirectory")
3939
@patch("shutil.copy2")
4040
def test_main(self, mock_copy, mock_temp, mock_recorder, mock_bundles, *mocks):
4141
mock_temp.return_value.__enter__.return_value.name = tempfile.gettempdir()
42-
mock_bundle = MagicMock(archive_path="path")
43-
mock_bundles.create.return_value = mock_bundle
42+
mock_bundle = MagicMock(min_dist=MagicMock(archive_path="path"))
43+
mock_bundles.return_value.__enter__.return_value = mock_bundle
4444

4545
main()
4646

4747
mock_bundle.install_min.assert_called()
4848
mock_bundle.install_plugins.assert_called()
4949

50-
mock_bundle.build_tar.assert_called_with(os.path.join("curdir", "dist"))
50+
mock_bundle.package.assert_called_with(os.path.join("curdir", "dist"))
5151

52-
mock_recorder.return_value.write_manifest.assert_has_calls([call("path"), call(os.path.join("curdir", "dist"))]) # manifest included in tar
52+
mock_recorder.return_value.write_manifest.assert_has_calls([
53+
call("path"),
54+
call(os.path.join("curdir", "dist"))
55+
]) # manifest included in package

0 commit comments

Comments
 (0)